mirror of https://github.com/docker/docker-py.git
Compare commits
254 Commits
Author | SHA1 | Date |
---|---|---|
|
6e6a273573 | |
|
526a9db743 | |
|
e5c3eb18b6 | |
|
820769e23c | |
|
db7f8b8bb6 | |
|
747d23b9d7 | |
|
fad84c371a | |
|
5a8a42466e | |
|
03e43be6af | |
|
80a584651b | |
|
8ee28517c7 | |
|
d9f9b965b2 | |
|
fba6ffe297 | |
|
99ce2e6d56 | |
|
504ce6193c | |
|
bb0edd1f66 | |
|
e47e966e94 | |
|
a8bac88221 | |
|
e031cf0c23 | |
|
b1265470e6 | |
|
6bbf741c8c | |
|
96ef4d3bee | |
|
a3652028b1 | |
|
1ab40c8e92 | |
|
b33088e0ca | |
|
45488acfc1 | |
|
20879eca6a | |
|
4f2a26d21e | |
|
7785ad913d | |
|
d8e9bcb278 | |
|
2a059a9f19 | |
|
e33e0a437e | |
|
b86573a3e3 | |
|
e34bcf20d9 | |
|
205d2f2bd0 | |
|
b6464dbed9 | |
|
9ad4bddc9e | |
|
336e65fc3c | |
|
4c6437d292 | |
|
0fd79c8c0d | |
|
3d79ce8c60 | |
|
dd82f9ae8e | |
|
e91b280074 | |
|
cb21af7f69 | |
|
1818712b8c | |
|
d50cc429c2 | |
|
047df6b0d3 | |
|
ae45d477c4 | |
|
f128956034 | |
|
bd164f928a | |
|
249654d4d9 | |
|
694d9792e6 | |
|
eeb9ea1937 | |
|
08956b5fbc | |
|
b8a6987cd5 | |
|
f467fd9df9 | |
|
3ec5a6849a | |
|
1784cc2962 | |
|
6ceb08273c | |
|
097382b973 | |
|
0fad869cc6 | |
|
2a5f354b50 | |
|
7d8a161b12 | |
|
5388413dde | |
|
3d0a3f1d77 | |
|
a9b5494fd0 | |
|
cb8f2c6630 | |
|
7140969239 | |
|
586988ce2d | |
|
fd2f5029f0 | |
|
db4878118b | |
|
976c84c481 | |
|
b3349c88ef | |
|
b2378db7f1 | |
|
911f866f72 | |
|
26e07251d4 | |
|
c9e3efddb8 | |
|
4a88112345 | |
|
b70cbd0129 | |
|
7752996f78 | |
|
5abae2dc8e | |
|
c38656dc78 | |
|
378325363e | |
|
0f0b20a6a7 | |
|
bea63224e0 | |
|
8b9ad7807f | |
|
c68d532f54 | |
|
a9a3775b15 | |
|
3948540c89 | |
|
0566f1260c | |
|
cc76c9c20d | |
|
09f12f2046 | |
|
6aec90a41b | |
|
8447f7b0f0 | |
|
601476733c | |
|
ec58856ee3 | |
|
fad792bfc7 | |
|
9313536601 | |
|
8a3402c049 | |
|
ee2310595d | |
|
dbc061f4fa | |
|
4571f7f9b4 | |
|
0618951093 | |
|
806d36a8cd | |
|
79c4c38b42 | |
|
62b4bb8489 | |
|
5064995bc4 | |
|
54ec0c6bf7 | |
|
83e93228ea | |
|
fb974de27a | |
|
f0d38fb7f4 | |
|
84414e343e | |
|
78439ebbe1 | |
|
0318ad8e7e | |
|
8ca9c6394f | |
|
bc4c0d7cf4 | |
|
14e8d07d45 | |
|
c5e582c413 | |
|
9cadad009e | |
|
443a35360f | |
|
e011ff5be8 | |
|
7870503c52 | |
|
a18f91bf08 | |
|
a662d5a305 | |
|
1d697680d2 | |
|
576e47aaac | |
|
3178c8d48b | |
|
a02ba74333 | |
|
aaf68b7f98 | |
|
f84623225e | |
|
7cd7458f2f | |
|
e9d4ddfaec | |
|
aca129dd69 | |
|
ee9151f336 | |
|
34e6829dd4 | |
|
22718ba59a | |
|
d38b41a13c | |
|
3afb4b61c3 | |
|
82cf559b5a | |
|
8590eaad3c | |
|
30022984f6 | |
|
bc0a5fbacd | |
|
923e067ddd | |
|
1c27ec1f0c | |
|
2494d63f36 | |
|
e901eac7a8 | |
|
fc86ab0d85 | |
|
45bf9f9115 | |
|
c03aeb659e | |
|
58aa62bb15 | |
|
ff0b4ac60b | |
|
66402435d1 | |
|
42789818be | |
|
ab5e927300 | |
|
b7daa52feb | |
|
3f0095a7c1 | |
|
631b332cd9 | |
|
7f1bde162f | |
|
cd2c35a9b6 | |
|
828d06f5f5 | |
|
dff849f6bb | |
|
52fb27690c | |
|
547cc5794d | |
|
003a16503a | |
|
c6c2bbdcda | |
|
73421027be | |
|
55f47299c4 | |
|
3ee3a2486f | |
|
868e996269 | |
|
26753c81de | |
|
0031ac2186 | |
|
b2a18d7209 | |
|
d69de54d7c | |
|
1a4cacdfb6 | |
|
26064dd6b5 | |
|
05e143429e | |
|
23cf16f03a | |
|
ab43018b02 | |
|
9bdb5ba2ba | |
|
be942f8390 | |
|
bf026265e0 | |
|
d2d097efbb | |
|
acdafbc116 | |
|
ea4cefe4fd | |
|
adf5a97b12 | |
|
d9298647d9 | |
|
bb40ba051f | |
|
52e29bd446 | |
|
da62a28837 | |
|
0ee9f260e4 | |
|
b9ca58a56d | |
|
cf6210316f | |
|
2e6dad7983 | |
|
4e19cc48df | |
|
56dd6de7df | |
|
bb11197ee3 | |
|
3ffdd8a1c5 | |
|
ce40d4bb34 | |
|
4765f62441 | |
|
74e0c5eb8c | |
|
7168e09b16 | |
|
f16c4e1147 | |
|
2933af2ca7 | |
|
a6db044bd4 | |
|
e131955685 | |
|
e0a3abfc37 | |
|
a48a5a9647 | |
|
ac5f6ef93a | |
|
4bb99311e2 | |
|
bbbc29191a | |
|
72bcd1616d | |
|
4150fc4d9d | |
|
a9de343210 | |
|
ecace769f5 | |
|
7172269b06 | |
|
fcb35f4197 | |
|
3c5f0d0ee1 | |
|
7779b84e87 | |
|
df59f538c2 | |
|
aae6be0c58 | |
|
b8258679b3 | |
|
b27faa62e7 | |
|
63618b5e11 | |
|
a9265197d2 | |
|
264688e37c | |
|
d06db4d9e1 | |
|
dbb28a5af1 | |
|
f9b85586ca | |
|
c5fc193857 | |
|
3c3aa69997 | |
|
4a3cddf4bf | |
|
62af2bbb13 | |
|
8da03e0126 | |
|
5705d12813 | |
|
2fa56879a2 | |
|
e0d186d754 | |
|
1abeb46dfa | |
|
582f6277ce | |
|
2cf3900030 | |
|
19d6cd8a01 | |
|
a9748a8b70 | |
|
5fcc293ba2 | |
|
650aad3a5f | |
|
f42a81dca2 | |
|
d58ca97207 | |
|
13c316de69 | |
|
7ac8b56730 | |
|
f53e615e0f | |
|
50a0ff596f | |
|
4b44fa7e5d | |
|
bf1a3518f9 | |
|
656db96b4a | |
|
755fd73566 | |
|
19171d0e1e |
|
@ -9,3 +9,6 @@ max_line_length = 80
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
# GitHub code owners
|
|
||||||
# See https://help.github.com/articles/about-codeowners/
|
|
||||||
#
|
|
||||||
# KEEP THIS FILE SORTED. Order is important. Last match takes precedence.
|
|
||||||
|
|
||||||
* @aiordache @ulyssessouza
|
|
|
@ -2,26 +2,71 @@ name: Python package
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
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:
|
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
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
max-parallel: 1
|
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
allow-prereleases: true
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
pip3 install -r test-requirements.txt -r requirements.txt
|
pip3 install '.[ssh,dev]'
|
||||||
- name: Test with pytest
|
- name: Run unit tests
|
||||||
run: |
|
run: |
|
||||||
docker logout
|
docker logout
|
||||||
rm -rf ~/.docker
|
rm -rf ~/.docker
|
||||||
py.test -v --cov=docker tests/unit
|
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 }}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
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 }}
|
|
@ -13,6 +13,10 @@ html/*
|
||||||
_build/
|
_build/
|
||||||
README.rst
|
README.rst
|
||||||
|
|
||||||
|
# setuptools_scm
|
||||||
|
_version.py
|
||||||
|
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
|
@ -3,8 +3,15 @@ version: 2
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: '3.12'
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.6
|
|
||||||
install:
|
install:
|
||||||
- requirements: docs-requirements.txt
|
- method: pip
|
||||||
- requirements: requirements.txt
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- ssh
|
||||||
|
- docs
|
||||||
|
|
|
@ -44,7 +44,7 @@ paragraph in the Docker contribution guidelines.
|
||||||
Before we can review your pull request, please ensure that nothing has been
|
Before we can review your pull request, please ensure that nothing has been
|
||||||
broken by your changes by running the test suite. You can do so simply by
|
broken by your changes by running the test suite. You can do so simply by
|
||||||
running `make test` in the project root. This also includes coding style using
|
running `make test` in the project root. This also includes coding style using
|
||||||
`flake8`
|
`ruff`
|
||||||
|
|
||||||
### 3. Write clear, self-contained commits
|
### 3. Write clear, self-contained commits
|
||||||
|
|
||||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -1,15 +1,13 @@
|
||||||
ARG PYTHON_VERSION=3.7
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
ARG PYTHON_VERSION=3.12
|
||||||
FROM python:${PYTHON_VERSION}
|
FROM python:${PYTHON_VERSION}
|
||||||
|
|
||||||
RUN mkdir /src
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
COPY requirements.txt /src/requirements.txt
|
ARG VERSION=0.0.0.dev0
|
||||||
RUN pip install -r requirements.txt
|
RUN --mount=type=cache,target=/cache/pip \
|
||||||
|
PIP_CACHE_DIR=/cache/pip \
|
||||||
COPY test-requirements.txt /src/test-requirements.txt
|
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \
|
||||||
RUN pip install -r test-requirements.txt
|
pip install .[ssh]
|
||||||
|
|
||||||
COPY . /src
|
|
||||||
RUN pip install .
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
ARG PYTHON_VERSION=3.7
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
ARG PYTHON_VERSION=3.12
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}
|
FROM python:${PYTHON_VERSION}
|
||||||
|
|
||||||
|
@ -9,7 +11,12 @@ RUN addgroup --gid $gid sphinx \
|
||||||
&& useradd --uid $uid --gid $gid -M sphinx
|
&& useradd --uid $uid --gid $gid -M sphinx
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY requirements.txt docs-requirements.txt ./
|
COPY . .
|
||||||
RUN pip install -r requirements.txt -r docs-requirements.txt
|
|
||||||
|
ARG VERSION=0.0.0.dev0
|
||||||
|
RUN --mount=type=cache,target=/cache/pip \
|
||||||
|
PIP_CACHE_DIR=/cache/pip \
|
||||||
|
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \
|
||||||
|
pip install .[ssh,docs]
|
||||||
|
|
||||||
USER sphinx
|
USER sphinx
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
#!groovy
|
|
||||||
|
|
||||||
def imageNameBase = "dockerpinata/docker-py"
|
|
||||||
def imageNamePy3
|
|
||||||
def imageDindSSH
|
|
||||||
def images = [:]
|
|
||||||
|
|
||||||
def buildImage = { name, buildargs, pyTag ->
|
|
||||||
img = docker.image(name)
|
|
||||||
try {
|
|
||||||
img.pull()
|
|
||||||
} catch (Exception exc) {
|
|
||||||
img = docker.build(name, buildargs)
|
|
||||||
img.push()
|
|
||||||
}
|
|
||||||
if (pyTag?.trim()) images[pyTag] = img.id
|
|
||||||
}
|
|
||||||
|
|
||||||
def buildImages = { ->
|
|
||||||
wrappedNode(label: "amd64 && ubuntu-2004 && overlay2", cleanWorkspace: true) {
|
|
||||||
stage("build image") {
|
|
||||||
checkout(scm)
|
|
||||||
|
|
||||||
imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}"
|
|
||||||
imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}"
|
|
||||||
withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
|
|
||||||
buildImage(imageDindSSH, "-f tests/Dockerfile-ssh-dind .", "")
|
|
||||||
buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.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: 'py3.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)
|
|
16
MAINTAINERS
16
MAINTAINERS
|
@ -11,17 +11,19 @@
|
||||||
[Org]
|
[Org]
|
||||||
[Org."Core maintainers"]
|
[Org."Core maintainers"]
|
||||||
people = [
|
people = [
|
||||||
"aiordache",
|
"glours",
|
||||||
"ulyssessouza",
|
"milas",
|
||||||
]
|
]
|
||||||
[Org.Alumni]
|
[Org.Alumni]
|
||||||
people = [
|
people = [
|
||||||
|
"aiordache",
|
||||||
"aanand",
|
"aanand",
|
||||||
"bfirsh",
|
"bfirsh",
|
||||||
"dnephin",
|
"dnephin",
|
||||||
"mnowster",
|
"mnowster",
|
||||||
"mpetazzoni",
|
"mpetazzoni",
|
||||||
"shin-",
|
"shin-",
|
||||||
|
"ulyssessouza",
|
||||||
]
|
]
|
||||||
|
|
||||||
[people]
|
[people]
|
||||||
|
@ -52,6 +54,16 @@
|
||||||
Email = "dnephin@gmail.com"
|
Email = "dnephin@gmail.com"
|
||||||
GitHub = "dnephin"
|
GitHub = "dnephin"
|
||||||
|
|
||||||
|
[people.glours]
|
||||||
|
Name = "Guillaume Lours"
|
||||||
|
Email = "705411+glours@users.noreply.github.com"
|
||||||
|
GitHub = "glours"
|
||||||
|
|
||||||
|
[people.milas]
|
||||||
|
Name = "Milas Bowman"
|
||||||
|
Email = "devnull@milas.dev"
|
||||||
|
GitHub = "milas"
|
||||||
|
|
||||||
[people.mnowster]
|
[people.mnowster]
|
||||||
Name = "Mazz Mosley"
|
Name = "Mazz Mosley"
|
||||||
Email = "mazz@houseofmnowster.com"
|
Email = "mazz@houseofmnowster.com"
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
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 *
|
|
188
Makefile
188
Makefile
|
@ -1,39 +1,75 @@
|
||||||
TEST_API_VERSION ?= 1.39
|
TEST_API_VERSION ?= 1.45
|
||||||
TEST_ENGINE_VERSION ?= 19.03.13
|
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
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: test
|
all: test
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
-docker rm -f dpy-dind-py3 dpy-dind-certs dpy-dind-ssl
|
-docker rm -f dpy-dind dpy-dind-certs dpy-dind-ssl
|
||||||
find -name "__pycache__" | xargs rm -rf
|
find -name "__pycache__" | xargs rm -rf
|
||||||
|
|
||||||
.PHONY: build-dind-ssh
|
.PHONY: build-dind-ssh
|
||||||
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 .
|
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-py3
|
.PHONY: build
|
||||||
build-py3:
|
build:
|
||||||
docker build -t docker-sdk-python3 -f tests/Dockerfile --build-arg APT_MIRROR .
|
docker build \
|
||||||
|
--pull \
|
||||||
|
-t docker-sdk-python3 \
|
||||||
|
-f tests/Dockerfile \
|
||||||
|
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
|
||||||
|
.
|
||||||
|
|
||||||
.PHONY: build-docs
|
.PHONY: build-docs
|
||||||
build-docs:
|
build-docs:
|
||||||
docker build -t docker-sdk-python-docs -f Dockerfile-docs --build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g) .
|
docker build \
|
||||||
|
-t docker-sdk-python-docs \
|
||||||
|
-f Dockerfile-docs \
|
||||||
|
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
|
||||||
|
$(uid_args) \
|
||||||
|
.
|
||||||
|
|
||||||
.PHONY: build-dind-certs
|
.PHONY: build-dind-certs
|
||||||
build-dind-certs:
|
build-dind-certs:
|
||||||
docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
|
docker build \
|
||||||
|
-t dpy-dind-certs \
|
||||||
|
-f tests/Dockerfile-dind-certs \
|
||||||
|
--build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \
|
||||||
|
.
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: flake8 unit-test-py3 integration-dind integration-dind-ssl
|
test: ruff unit-test integration-dind integration-dind-ssl
|
||||||
|
|
||||||
.PHONY: unit-test-py3
|
.PHONY: unit-test
|
||||||
unit-test-py3: build-py3
|
unit-test: build
|
||||||
docker run -t --rm docker-sdk-python3 py.test tests/unit
|
docker run -t --rm docker-sdk-python3 py.test tests/unit
|
||||||
|
|
||||||
.PHONY: integration-test-py3
|
.PHONY: integration-test
|
||||||
integration-test-py3: build-py3
|
integration-test: build
|
||||||
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
|
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
|
||||||
|
|
||||||
.PHONY: setup-network
|
.PHONY: setup-network
|
||||||
|
@ -41,53 +77,113 @@ setup-network:
|
||||||
docker network inspect dpy-tests || docker network create dpy-tests
|
docker network inspect dpy-tests || docker network create dpy-tests
|
||||||
|
|
||||||
.PHONY: integration-dind
|
.PHONY: integration-dind
|
||||||
integration-dind: integration-dind-py3
|
integration-dind: build setup-network
|
||||||
|
docker rm -vf dpy-dind || :
|
||||||
|
|
||||||
.PHONY: integration-dind-py3
|
docker run \
|
||||||
integration-dind-py3: build-py3 setup-network
|
--detach \
|
||||||
docker rm -vf dpy-dind-py3 || :
|
--name dpy-dind \
|
||||||
docker run -d --network dpy-tests --name dpy-dind-py3 --privileged\
|
--network dpy-tests \
|
||||||
docker:${TEST_ENGINE_VERSION}-dind dockerd -H tcp://0.0.0.0:2375 --experimental
|
--pull=always \
|
||||||
docker run -t --rm --env="DOCKER_HOST=tcp://dpy-dind-py3:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
|
--privileged \
|
||||||
--network dpy-tests docker-sdk-python3 py.test tests/integration/${file}
|
docker:${TEST_ENGINE_VERSION}-dind \
|
||||||
docker rm -vf dpy-dind-py3
|
dockerd -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 2375; do sleep 1; done'
|
||||||
|
|
||||||
|
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-ssh-py3
|
.PHONY: integration-dind-ssh
|
||||||
integration-ssh-py3: build-dind-ssh build-py3 setup-network
|
integration-dind-ssh: build-dind-ssh build setup-network
|
||||||
docker rm -vf dpy-dind-py3 || :
|
docker rm -vf dpy-dind-ssh || :
|
||||||
docker run -d --network dpy-tests --name dpy-dind-py3 --privileged\
|
docker run -d --network dpy-tests --name dpy-dind-ssh --privileged \
|
||||||
docker-dind-ssh dockerd --experimental
|
docker-dind-ssh dockerd --experimental
|
||||||
# start SSH daemon
|
# start SSH daemon for known key
|
||||||
docker exec dpy-dind-py3 sh -c "/usr/sbin/sshd"
|
docker exec dpy-dind-ssh sh -c "/usr/sbin/sshd -h /etc/ssh/known_ed25519 -p 22"
|
||||||
docker run -t --rm --env="DOCKER_HOST=ssh://dpy-dind-py3" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
|
docker exec dpy-dind-ssh sh -c "/usr/sbin/sshd -h /etc/ssh/unknown_ed25519 -p 2222"
|
||||||
--network dpy-tests docker-sdk-python3 py.test tests/ssh/${file}
|
docker run \
|
||||||
docker rm -vf dpy-dind-py3
|
--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
|
||||||
|
|
||||||
|
|
||||||
.PHONY: integration-dind-ssl
|
.PHONY: integration-dind-ssl
|
||||||
integration-dind-ssl: build-dind-certs build-py3
|
integration-dind-ssl: build-dind-certs build setup-network
|
||||||
docker rm -vf dpy-dind-certs dpy-dind-ssl || :
|
docker rm -vf dpy-dind-certs dpy-dind-ssl || :
|
||||||
docker run -d --name dpy-dind-certs dpy-dind-certs
|
docker run -d --name dpy-dind-certs dpy-dind-certs
|
||||||
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\
|
docker run \
|
||||||
--network dpy-tests --network-alias docker -v /tmp --privileged\
|
--detach \
|
||||||
docker:${TEST_ENGINE_VERSION}-dind\
|
--env="DOCKER_CERT_PATH=/certs" \
|
||||||
dockerd --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/server-cert.pem\
|
--env="DOCKER_HOST=tcp://localhost:2375" \
|
||||||
--tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375 --experimental
|
--env="DOCKER_TLS_VERIFY=1" \
|
||||||
docker run -t --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\
|
--name dpy-dind-ssl \
|
||||||
--env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
|
--network dpy-tests \
|
||||||
--network dpy-tests docker-sdk-python3 py.test tests/integration/${file}
|
--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 rm -vf dpy-dind-ssl dpy-dind-certs
|
docker rm -vf dpy-dind-ssl dpy-dind-certs
|
||||||
|
|
||||||
.PHONY: flake8
|
.PHONY: ruff
|
||||||
flake8: build-py3
|
ruff: build
|
||||||
docker run -t --rm docker-sdk-python3 flake8 docker tests
|
docker run -t --rm docker-sdk-python3 ruff docker tests
|
||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs: build-docs
|
docs: build-docs
|
||||||
docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build
|
docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build
|
||||||
|
|
||||||
.PHONY: shell
|
.PHONY: shell
|
||||||
shell: build-py3
|
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-python3 python
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
# Docker SDK for Python
|
# Docker SDK for Python
|
||||||
|
|
||||||
[](https://github.com/docker/docker-py/actions/workflows/ci.yml/)
|
[](https://github.com/docker/docker-py/actions/workflows/ci.yml)
|
||||||
|
|
||||||
A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps – run containers, manage containers, manage Swarms, etc.
|
A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps – run containers, manage containers, manage Swarms, etc.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Either add `docker` to your `requirements.txt` file or install with pip:
|
The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Install with pip:
|
||||||
|
|
||||||
pip install docker
|
pip install docker
|
||||||
|
|
||||||
If you are intending to connect to a docker host via TLS, add `docker[tls]` to your requirements instead, or install with pip:
|
> 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.
|
||||||
pip install docker[tls]
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
13
appveyor.yml
13
appveyor.yml
|
@ -1,13 +0,0 @@
|
||||||
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"
|
|
|
@ -1,10 +1,7 @@
|
||||||
# flake8: noqa
|
|
||||||
from .api import APIClient
|
from .api import APIClient
|
||||||
from .client import DockerClient, from_env
|
from .client import DockerClient, from_env
|
||||||
from .context import Context
|
from .context import Context, ContextAPI
|
||||||
from .context import ContextAPI
|
|
||||||
from .tls import TLSConfig
|
from .tls import TLSConfig
|
||||||
from .version import version, version_info
|
from .version import __version__
|
||||||
|
|
||||||
__version__ = version
|
|
||||||
__title__ = 'docker'
|
__title__ = 'docker'
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
# flake8: noqa
|
|
||||||
from .client import APIClient
|
from .client import APIClient
|
||||||
|
|
|
@ -3,16 +3,12 @@ import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from .. import auth
|
from .. import auth, constants, errors, utils
|
||||||
from .. import constants
|
|
||||||
from .. import errors
|
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BuildApiMixin(object):
|
class BuildApiMixin:
|
||||||
def build(self, path=None, tag=None, quiet=False, fileobj=None,
|
def build(self, path=None, tag=None, quiet=False, fileobj=None,
|
||||||
nocache=False, rm=False, timeout=None,
|
nocache=False, rm=False, timeout=None,
|
||||||
custom_context=False, encoding=None, pull=False,
|
custom_context=False, encoding=None, pull=False,
|
||||||
|
@ -76,6 +72,7 @@ class BuildApiMixin(object):
|
||||||
forcerm (bool): Always remove intermediate containers, even after
|
forcerm (bool): Always remove intermediate containers, even after
|
||||||
unsuccessful builds
|
unsuccessful builds
|
||||||
dockerfile (str): path within the build context to the Dockerfile
|
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
|
buildargs (dict): A dictionary of build arguments
|
||||||
container_limits (dict): A dictionary of limits applied to each
|
container_limits (dict): A dictionary of limits applied to each
|
||||||
container created by the build process. Valid keys:
|
container created by the build process. Valid keys:
|
||||||
|
@ -128,13 +125,16 @@ class BuildApiMixin(object):
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Can not use custom encoding if gzip is enabled'
|
'Can not use custom encoding if gzip is enabled'
|
||||||
)
|
)
|
||||||
|
if tag is not None:
|
||||||
|
if not utils.match_tag(tag):
|
||||||
|
raise errors.DockerException(
|
||||||
|
f"invalid tag '{tag}': invalid reference format"
|
||||||
|
)
|
||||||
for key in container_limits.keys():
|
for key in container_limits.keys():
|
||||||
if key not in constants.CONTAINER_LIMITS_KEYS:
|
if key not in constants.CONTAINER_LIMITS_KEYS:
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid container_limits key {0}'.format(key)
|
f"invalid tag '{tag}': invalid reference format"
|
||||||
)
|
)
|
||||||
|
|
||||||
if custom_context:
|
if custom_context:
|
||||||
if not fileobj:
|
if not fileobj:
|
||||||
raise TypeError("You must specify fileobj with custom_context")
|
raise TypeError("You must specify fileobj with custom_context")
|
||||||
|
@ -150,10 +150,10 @@ class BuildApiMixin(object):
|
||||||
dockerignore = os.path.join(path, '.dockerignore')
|
dockerignore = os.path.join(path, '.dockerignore')
|
||||||
exclude = None
|
exclude = None
|
||||||
if os.path.exists(dockerignore):
|
if os.path.exists(dockerignore):
|
||||||
with open(dockerignore, 'r') as f:
|
with open(dockerignore) as f:
|
||||||
exclude = list(filter(
|
exclude = list(filter(
|
||||||
lambda x: x != '' and x[0] != '#',
|
lambda x: x != '' and x[0] != '#',
|
||||||
[l.strip() for l in f.read().splitlines()]
|
[line.strip() for line in f.read().splitlines()]
|
||||||
))
|
))
|
||||||
dockerfile = process_dockerfile(dockerfile, path)
|
dockerfile = process_dockerfile(dockerfile, path)
|
||||||
context = utils.tar(
|
context = utils.tar(
|
||||||
|
@ -275,10 +275,24 @@ class BuildApiMixin(object):
|
||||||
return self._stream_helper(response, decode=decode)
|
return self._stream_helper(response, decode=decode)
|
||||||
|
|
||||||
@utils.minimum_version('1.31')
|
@utils.minimum_version('1.31')
|
||||||
def prune_builds(self):
|
def prune_builds(self, filters=None, keep_storage=None, all=None):
|
||||||
"""
|
"""
|
||||||
Delete the builder cache
|
Delete the builder cache
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filters (dict): Filters to process on the prune list.
|
||||||
|
Needs Docker API v1.39+
|
||||||
|
Available filters:
|
||||||
|
- dangling (bool): When set to true (or 1), prune only
|
||||||
|
unused and untagged images.
|
||||||
|
- until (str): Can be Unix timestamps, date formatted
|
||||||
|
timestamps, or Go duration strings (e.g. 10m, 1h30m) computed
|
||||||
|
relative to the daemon's local time.
|
||||||
|
keep_storage (int): Amount of disk space in bytes to keep for cache.
|
||||||
|
Needs Docker API v1.39+
|
||||||
|
all (bool): Remove all types of build cache.
|
||||||
|
Needs Docker API v1.39+
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(dict): A dictionary containing information about the operation's
|
(dict): A dictionary containing information about the operation's
|
||||||
result. The ``SpaceReclaimed`` key indicates the amount of
|
result. The ``SpaceReclaimed`` key indicates the amount of
|
||||||
|
@ -289,7 +303,20 @@ class BuildApiMixin(object):
|
||||||
If the server returns an error.
|
If the server returns an error.
|
||||||
"""
|
"""
|
||||||
url = self._url("/build/prune")
|
url = self._url("/build/prune")
|
||||||
return self._result(self._post(url), True)
|
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)
|
||||||
|
|
||||||
def _set_auth_headers(self, headers):
|
def _set_auth_headers(self, headers):
|
||||||
log.debug('Looking for auth config')
|
log.debug('Looking for auth config')
|
||||||
|
@ -313,9 +340,8 @@ class BuildApiMixin(object):
|
||||||
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
|
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'Sending auth config ({0})'.format(
|
"Sending auth config (%s)",
|
||||||
', '.join(repr(k) for k in auth_data.keys())
|
', '.join(repr(k) for k in auth_data),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if auth_data:
|
if auth_data:
|
||||||
|
@ -335,18 +361,15 @@ def process_dockerfile(dockerfile, path):
|
||||||
abs_dockerfile = os.path.join(path, dockerfile)
|
abs_dockerfile = os.path.join(path, dockerfile)
|
||||||
if constants.IS_WINDOWS_PLATFORM and path.startswith(
|
if constants.IS_WINDOWS_PLATFORM and path.startswith(
|
||||||
constants.WINDOWS_LONGPATH_PREFIX):
|
constants.WINDOWS_LONGPATH_PREFIX):
|
||||||
abs_dockerfile = '{}{}'.format(
|
normpath = os.path.normpath(
|
||||||
constants.WINDOWS_LONGPATH_PREFIX,
|
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):])
|
||||||
os.path.normpath(
|
abs_dockerfile = f'{constants.WINDOWS_LONGPATH_PREFIX}{normpath}'
|
||||||
abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
|
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
|
||||||
os.path.relpath(abs_dockerfile, path).startswith('..')):
|
os.path.relpath(abs_dockerfile, path).startswith('..')):
|
||||||
# Dockerfile not in context - read data to insert into tar later
|
# Dockerfile not in context - read data to insert into tar later
|
||||||
with open(abs_dockerfile, 'r') as df:
|
with open(abs_dockerfile) as df:
|
||||||
return (
|
return (
|
||||||
'.dockerfile.{0:x}'.format(random.getrandbits(160)),
|
f'.dockerfile.{random.getrandbits(160):x}',
|
||||||
df.read()
|
df.read()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,28 @@ import urllib
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import requests.adapters
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
import websocket
|
|
||||||
|
|
||||||
from .. import auth
|
from .. import auth
|
||||||
from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
|
from ..constants import (
|
||||||
DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
|
DEFAULT_MAX_POOL_SIZE,
|
||||||
DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
|
DEFAULT_NUM_POOLS,
|
||||||
MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES)
|
DEFAULT_NUM_POOLS_SSH,
|
||||||
from ..errors import (DockerException, InvalidVersion, TLSParameterError,
|
DEFAULT_TIMEOUT_SECONDS,
|
||||||
create_api_error_from_http_exception)
|
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 ..tls import TLSConfig
|
||||||
from ..transport import SSLHTTPAdapter, UnixHTTPAdapter
|
from ..transport import UnixHTTPAdapter
|
||||||
from ..utils import check_resource, config, update_headers, utils
|
from ..utils import check_resource, config, update_headers, utils
|
||||||
from ..utils.json_stream import json_stream
|
from ..utils.json_stream import json_stream
|
||||||
from ..utils.proxy import ProxyConfig
|
from ..utils.proxy import ProxyConfig
|
||||||
|
@ -107,7 +117,7 @@ class APIClient(
|
||||||
user_agent=DEFAULT_USER_AGENT, num_pools=None,
|
user_agent=DEFAULT_USER_AGENT, num_pools=None,
|
||||||
credstore_env=None, use_ssh_client=False,
|
credstore_env=None, use_ssh_client=False,
|
||||||
max_pool_size=DEFAULT_MAX_POOL_SIZE):
|
max_pool_size=DEFAULT_MAX_POOL_SIZE):
|
||||||
super(APIClient, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
if tls and not base_url:
|
if tls and not base_url:
|
||||||
raise TLSParameterError(
|
raise TLSParameterError(
|
||||||
|
@ -160,10 +170,10 @@ class APIClient(
|
||||||
base_url, timeout, pool_connections=num_pools,
|
base_url, timeout, pool_connections=num_pools,
|
||||||
max_pool_size=max_pool_size
|
max_pool_size=max_pool_size
|
||||||
)
|
)
|
||||||
except NameError:
|
except NameError as err:
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Install pypiwin32 package to enable npipe:// support'
|
'Install pypiwin32 package to enable npipe:// support'
|
||||||
)
|
) from err
|
||||||
self.mount('http+docker://', self._custom_adapter)
|
self.mount('http+docker://', self._custom_adapter)
|
||||||
self.base_url = 'http+docker://localnpipe'
|
self.base_url = 'http+docker://localnpipe'
|
||||||
elif base_url.startswith('ssh://'):
|
elif base_url.startswith('ssh://'):
|
||||||
|
@ -172,10 +182,10 @@ class APIClient(
|
||||||
base_url, timeout, pool_connections=num_pools,
|
base_url, timeout, pool_connections=num_pools,
|
||||||
max_pool_size=max_pool_size, shell_out=use_ssh_client
|
max_pool_size=max_pool_size, shell_out=use_ssh_client
|
||||||
)
|
)
|
||||||
except NameError:
|
except NameError as err:
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Install paramiko package to enable ssh:// support'
|
'Install paramiko package to enable ssh:// support'
|
||||||
)
|
) from err
|
||||||
self.mount('http+docker://ssh', self._custom_adapter)
|
self.mount('http+docker://ssh', self._custom_adapter)
|
||||||
self._unmount('http://', 'https://')
|
self._unmount('http://', 'https://')
|
||||||
self.base_url = 'http+docker://ssh'
|
self.base_url = 'http+docker://ssh'
|
||||||
|
@ -184,7 +194,7 @@ class APIClient(
|
||||||
if isinstance(tls, TLSConfig):
|
if isinstance(tls, TLSConfig):
|
||||||
tls.configure_client(self)
|
tls.configure_client(self)
|
||||||
elif tls:
|
elif tls:
|
||||||
self._custom_adapter = SSLHTTPAdapter(
|
self._custom_adapter = requests.adapters.HTTPAdapter(
|
||||||
pool_connections=num_pools)
|
pool_connections=num_pools)
|
||||||
self.mount('https://', self._custom_adapter)
|
self.mount('https://', self._custom_adapter)
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
@ -199,28 +209,27 @@ class APIClient(
|
||||||
self._version = version
|
self._version = version
|
||||||
if not isinstance(self._version, str):
|
if not isinstance(self._version, str):
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Version parameter must be a string or None. Found {0}'.format(
|
'Version parameter must be a string or None. '
|
||||||
type(version).__name__
|
f'Found {type(version).__name__}'
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
|
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
|
||||||
raise InvalidVersion(
|
raise InvalidVersion(
|
||||||
'API versions below {} are no longer supported by this '
|
f'API versions below {MINIMUM_DOCKER_API_VERSION} are '
|
||||||
'library.'.format(MINIMUM_DOCKER_API_VERSION)
|
f'no longer supported by this library.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def _retrieve_server_version(self):
|
def _retrieve_server_version(self):
|
||||||
try:
|
try:
|
||||||
return self.version(api_version=False)["ApiVersion"]
|
return self.version(api_version=False)["ApiVersion"]
|
||||||
except KeyError:
|
except KeyError as ke:
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Invalid response from docker daemon: key "ApiVersion"'
|
'Invalid response from docker daemon: key "ApiVersion"'
|
||||||
' is missing.'
|
' is missing.'
|
||||||
)
|
) from ke
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Error while fetching server API version: {0}'.format(e)
|
f'Error while fetching server API version: {e}'
|
||||||
)
|
) from e
|
||||||
|
|
||||||
def _set_request_timeout(self, kwargs):
|
def _set_request_timeout(self, kwargs):
|
||||||
"""Prepare the kwargs for an HTTP request by inserting the timeout
|
"""Prepare the kwargs for an HTTP request by inserting the timeout
|
||||||
|
@ -248,26 +257,24 @@ class APIClient(
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if not isinstance(arg, str):
|
if not isinstance(arg, str):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Expected a string but found {0} ({1}) '
|
f'Expected a string but found {arg} ({type(arg)}) instead'
|
||||||
'instead'.format(arg, type(arg))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
quote_f = partial(urllib.parse.quote, safe="/:")
|
quote_f = partial(urllib.parse.quote, safe="/:")
|
||||||
args = map(quote_f, args)
|
args = map(quote_f, args)
|
||||||
|
|
||||||
|
formatted_path = pathfmt.format(*args)
|
||||||
if kwargs.get('versioned_api', True):
|
if kwargs.get('versioned_api', True):
|
||||||
return '{0}/v{1}{2}'.format(
|
return f'{self.base_url}/v{self._version}{formatted_path}'
|
||||||
self.base_url, self._version, pathfmt.format(*args)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return '{0}{1}'.format(self.base_url, pathfmt.format(*args))
|
return f'{self.base_url}{formatted_path}'
|
||||||
|
|
||||||
def _raise_for_status(self, response):
|
def _raise_for_status(self, response):
|
||||||
"""Raises stored :class:`APIError`, if one occurred."""
|
"""Raises stored :class:`APIError`, if one occurred."""
|
||||||
try:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
raise create_api_error_from_http_exception(e)
|
raise create_api_error_from_http_exception(e) from e
|
||||||
|
|
||||||
def _result(self, response, json=False, binary=False):
|
def _result(self, response, json=False, binary=False):
|
||||||
assert not (json and binary)
|
assert not (json and binary)
|
||||||
|
@ -312,7 +319,16 @@ class APIClient(
|
||||||
return self._create_websocket_connection(full_url)
|
return self._create_websocket_connection(full_url)
|
||||||
|
|
||||||
def _create_websocket_connection(self, url):
|
def _create_websocket_connection(self, url):
|
||||||
return websocket.create_connection(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
|
||||||
|
|
||||||
def _get_raw_response_socket(self, response):
|
def _get_raw_response_socket(self, response):
|
||||||
self._raise_for_status(response)
|
self._raise_for_status(response)
|
||||||
|
@ -341,8 +357,7 @@ class APIClient(
|
||||||
|
|
||||||
if response.raw._fp.chunked:
|
if response.raw._fp.chunked:
|
||||||
if decode:
|
if decode:
|
||||||
for chunk in json_stream(self._stream_helper(response, False)):
|
yield from json_stream(self._stream_helper(response, False))
|
||||||
yield chunk
|
|
||||||
else:
|
else:
|
||||||
reader = response.raw
|
reader = response.raw
|
||||||
while not reader.closed:
|
while not reader.closed:
|
||||||
|
@ -398,10 +413,19 @@ class APIClient(
|
||||||
def _stream_raw_result(self, response, chunk_size=1, decode=True):
|
def _stream_raw_result(self, response, chunk_size=1, decode=True):
|
||||||
''' Stream result for TTY-enabled container and raw binary data'''
|
''' Stream result for TTY-enabled container and raw binary data'''
|
||||||
self._raise_for_status(response)
|
self._raise_for_status(response)
|
||||||
for out in response.iter_content(chunk_size, decode):
|
|
||||||
yield out
|
# 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)
|
||||||
|
|
||||||
def _read_from_socket(self, response, stream, tty=True, demux=False):
|
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)
|
socket = self._get_raw_response_socket(response)
|
||||||
|
|
||||||
gen = frames_iter(socket, tty)
|
gen = frames_iter(socket, tty)
|
||||||
|
@ -416,8 +440,11 @@ class APIClient(
|
||||||
if stream:
|
if stream:
|
||||||
return gen
|
return gen
|
||||||
else:
|
else:
|
||||||
# Wait for all the frames, concatenate them, and return the result
|
try:
|
||||||
return consume_socket_output(gen, demux=demux)
|
# Wait for all frames, concatenate them, and return the result
|
||||||
|
return consume_socket_output(gen, demux=demux)
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
|
||||||
def _disable_socket_timeout(self, socket):
|
def _disable_socket_timeout(self, socket):
|
||||||
""" Depending on the combination of python version and whether we're
|
""" Depending on the combination of python version and whether we're
|
||||||
|
@ -468,7 +495,7 @@ class APIClient(
|
||||||
return self._multiplexed_response_stream_helper(res)
|
return self._multiplexed_response_stream_helper(res)
|
||||||
else:
|
else:
|
||||||
return sep.join(
|
return sep.join(
|
||||||
[x for x in self._multiplexed_buffer_helper(res)]
|
list(self._multiplexed_buffer_helper(res))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _unmount(self, *args):
|
def _unmount(self, *args):
|
||||||
|
@ -477,7 +504,7 @@ class APIClient(
|
||||||
|
|
||||||
def get_adapter(self, url):
|
def get_adapter(self, url):
|
||||||
try:
|
try:
|
||||||
return super(APIClient, self).get_adapter(url)
|
return super().get_adapter(url)
|
||||||
except requests.exceptions.InvalidSchema as e:
|
except requests.exceptions.InvalidSchema as e:
|
||||||
if self._custom_adapter:
|
if self._custom_adapter:
|
||||||
return self._custom_adapter
|
return self._custom_adapter
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
class ConfigApiMixin(object):
|
class ConfigApiMixin:
|
||||||
@utils.minimum_version('1.30')
|
@utils.minimum_version('1.30')
|
||||||
def create_config(self, name, data, labels=None):
|
def create_config(self, name, data, labels=None, templating=None):
|
||||||
"""
|
"""
|
||||||
Create a config
|
Create a config
|
||||||
|
|
||||||
|
@ -15,6 +13,9 @@ class ConfigApiMixin(object):
|
||||||
name (string): Name of the config
|
name (string): Name of the config
|
||||||
data (bytes): Config data to be stored
|
data (bytes): Config data to be stored
|
||||||
labels (dict): A mapping of labels to assign to the config
|
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
|
Returns (dict): ID of the newly created config
|
||||||
"""
|
"""
|
||||||
|
@ -22,12 +23,12 @@ class ConfigApiMixin(object):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
data = base64.b64encode(data)
|
data = base64.b64encode(data)
|
||||||
if six.PY3:
|
data = data.decode('ascii')
|
||||||
data = data.decode('ascii')
|
|
||||||
body = {
|
body = {
|
||||||
'Data': data,
|
'Data': data,
|
||||||
'Name': name,
|
'Name': name,
|
||||||
'Labels': labels
|
'Labels': labels,
|
||||||
|
'Templating': templating
|
||||||
}
|
}
|
||||||
|
|
||||||
url = self._url('/configs/create')
|
url = self._url('/configs/create')
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import six
|
from .. import errors, utils
|
||||||
|
|
||||||
from .. import errors
|
|
||||||
from .. import utils
|
|
||||||
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
||||||
from ..types import CancellableStream
|
from ..types import (
|
||||||
from ..types import ContainerConfig
|
CancellableStream,
|
||||||
from ..types import EndpointConfig
|
ContainerConfig,
|
||||||
from ..types import HostConfig
|
EndpointConfig,
|
||||||
from ..types import NetworkingConfig
|
HostConfig,
|
||||||
|
NetworkingConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContainerApiMixin(object):
|
class ContainerApiMixin:
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def attach(self, container, stdout=True, stderr=True,
|
def attach(self, container, stdout=True, stderr=True,
|
||||||
stream=False, logs=False, demux=False):
|
stream=False, logs=False, demux=False):
|
||||||
|
@ -114,7 +113,7 @@ class ContainerApiMixin(object):
|
||||||
|
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def commit(self, container, repository=None, tag=None, message=None,
|
def commit(self, container, repository=None, tag=None, message=None,
|
||||||
author=None, changes=None, conf=None):
|
author=None, pause=True, changes=None, conf=None):
|
||||||
"""
|
"""
|
||||||
Commit a container to an image. Similar to the ``docker commit``
|
Commit a container to an image. Similar to the ``docker commit``
|
||||||
command.
|
command.
|
||||||
|
@ -125,6 +124,7 @@ class ContainerApiMixin(object):
|
||||||
tag (str): The tag to push
|
tag (str): The tag to push
|
||||||
message (str): A commit message
|
message (str): A commit message
|
||||||
author (str): The name of the author
|
author (str): The name of the author
|
||||||
|
pause (bool): Whether to pause the container before committing
|
||||||
changes (str): Dockerfile instructions to apply while committing
|
changes (str): Dockerfile instructions to apply while committing
|
||||||
conf (dict): The configuration for the container. See the
|
conf (dict): The configuration for the container. See the
|
||||||
`Engine API documentation
|
`Engine API documentation
|
||||||
|
@ -141,6 +141,7 @@ class ContainerApiMixin(object):
|
||||||
'tag': tag,
|
'tag': tag,
|
||||||
'comment': message,
|
'comment': message,
|
||||||
'author': author,
|
'author': author,
|
||||||
|
'pause': pause,
|
||||||
'changes': changes
|
'changes': changes
|
||||||
}
|
}
|
||||||
u = self._url("/commit")
|
u = self._url("/commit")
|
||||||
|
@ -225,7 +226,7 @@ class ContainerApiMixin(object):
|
||||||
mac_address=None, labels=None, stop_signal=None,
|
mac_address=None, labels=None, stop_signal=None,
|
||||||
networking_config=None, healthcheck=None,
|
networking_config=None, healthcheck=None,
|
||||||
stop_timeout=None, runtime=None,
|
stop_timeout=None, runtime=None,
|
||||||
use_config_proxy=True):
|
use_config_proxy=True, platform=None):
|
||||||
"""
|
"""
|
||||||
Creates a container. Parameters are similar to those for the ``docker
|
Creates a container. Parameters are similar to those for the ``docker
|
||||||
run`` command except it doesn't support the attach options (``-a``).
|
run`` command except it doesn't support the attach options (``-a``).
|
||||||
|
@ -258,7 +259,9 @@ class ContainerApiMixin(object):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
client.api.create_host_config(port_bindings={1111: ('127.0.0.1', 4567)})
|
client.api.create_host_config(
|
||||||
|
port_bindings={1111: ('127.0.0.1', 4567)}
|
||||||
|
)
|
||||||
|
|
||||||
Or without host port assignment:
|
Or without host port assignment:
|
||||||
|
|
||||||
|
@ -317,6 +320,11 @@ class ContainerApiMixin(object):
|
||||||
'/var/www': {
|
'/var/www': {
|
||||||
'bind': '/mnt/vol1',
|
'bind': '/mnt/vol1',
|
||||||
'mode': 'ro',
|
'mode': 'ro',
|
||||||
|
},
|
||||||
|
'/autofs/user1': {
|
||||||
|
'bind': '/mnt/vol3',
|
||||||
|
'mode': 'rw',
|
||||||
|
'propagation': 'shared'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -327,10 +335,11 @@ class ContainerApiMixin(object):
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
container_id = client.api.create_container(
|
container_id = client.api.create_container(
|
||||||
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
|
'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2', '/mnt/vol3'],
|
||||||
host_config=client.api.create_host_config(binds=[
|
host_config=client.api.create_host_config(binds=[
|
||||||
'/home/user1/:/mnt/vol2',
|
'/home/user1/:/mnt/vol2',
|
||||||
'/var/www:/mnt/vol1:ro',
|
'/var/www:/mnt/vol1:ro',
|
||||||
|
'/autofs/user1:/mnt/vol3:rw,shared',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -398,6 +407,7 @@ class ContainerApiMixin(object):
|
||||||
configuration file (``~/.docker/config.json`` by default)
|
configuration file (``~/.docker/config.json`` by default)
|
||||||
contains a proxy configuration, the corresponding environment
|
contains a proxy configuration, the corresponding environment
|
||||||
variables will be set in the container being created.
|
variables will be set in the container being created.
|
||||||
|
platform (str): Platform in the format ``os[/arch[/variant]]``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary with an image 'Id' key and a 'Warnings' key.
|
A dictionary with an image 'Id' key and a 'Warnings' key.
|
||||||
|
@ -408,7 +418,7 @@ class ContainerApiMixin(object):
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
If the server returns an error.
|
If the server returns an error.
|
||||||
"""
|
"""
|
||||||
if isinstance(volumes, six.string_types):
|
if isinstance(volumes, str):
|
||||||
volumes = [volumes, ]
|
volumes = [volumes, ]
|
||||||
|
|
||||||
if isinstance(environment, dict):
|
if isinstance(environment, dict):
|
||||||
|
@ -427,16 +437,22 @@ class ContainerApiMixin(object):
|
||||||
stop_signal, networking_config, healthcheck,
|
stop_signal, networking_config, healthcheck,
|
||||||
stop_timeout, runtime
|
stop_timeout, runtime
|
||||||
)
|
)
|
||||||
return self.create_container_from_config(config, name)
|
return self.create_container_from_config(config, name, platform)
|
||||||
|
|
||||||
def create_container_config(self, *args, **kwargs):
|
def create_container_config(self, *args, **kwargs):
|
||||||
return ContainerConfig(self._version, *args, **kwargs)
|
return ContainerConfig(self._version, *args, **kwargs)
|
||||||
|
|
||||||
def create_container_from_config(self, config, name=None):
|
def create_container_from_config(self, config, name=None, platform=None):
|
||||||
u = self._url("/containers/create")
|
u = self._url("/containers/create")
|
||||||
params = {
|
params = {
|
||||||
'name': name
|
'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)
|
res = self._post_json(u, data=config, params=params)
|
||||||
return self._result(res, True)
|
return self._result(res, True)
|
||||||
|
|
||||||
|
@ -581,10 +597,13 @@ class ContainerApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> client.api.create_host_config(privileged=True, cap_drop=['MKNOD'],
|
>>> client.api.create_host_config(
|
||||||
volumes_from=['nostalgic_newton'])
|
... privileged=True,
|
||||||
|
... cap_drop=['MKNOD'],
|
||||||
|
... volumes_from=['nostalgic_newton'],
|
||||||
|
... )
|
||||||
{'CapDrop': ['MKNOD'], 'LxcConf': None, 'Privileged': True,
|
{'CapDrop': ['MKNOD'], 'LxcConf': None, 'Privileged': True,
|
||||||
'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
|
'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not kwargs:
|
if not kwargs:
|
||||||
|
@ -668,7 +687,8 @@ class ContainerApiMixin(object):
|
||||||
container (str): The container to diff
|
container (str): The container to diff
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(str)
|
(list) A list of dictionaries containing the attributes `Path`
|
||||||
|
and `Kind`.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -790,7 +810,7 @@ class ContainerApiMixin(object):
|
||||||
url = self._url("/containers/{0}/kill", container)
|
url = self._url("/containers/{0}/kill", container)
|
||||||
params = {}
|
params = {}
|
||||||
if signal is not None:
|
if signal is not None:
|
||||||
if not isinstance(signal, six.string_types):
|
if not isinstance(signal, str):
|
||||||
signal = int(signal)
|
signal = int(signal)
|
||||||
params['signal'] = signal
|
params['signal'] = signal
|
||||||
res = self._post(url, params=params)
|
res = self._post(url, params=params)
|
||||||
|
@ -816,14 +836,15 @@ class ContainerApiMixin(object):
|
||||||
tail (str or int): Output specified number of lines at the end of
|
tail (str or int): Output specified number of lines at the end of
|
||||||
logs. Either an integer of number of lines or the string
|
logs. Either an integer of number of lines or the string
|
||||||
``all``. Default ``all``
|
``all``. Default ``all``
|
||||||
since (datetime or int): Show logs since a given datetime or
|
since (datetime, int, or float): Show logs since a given datetime,
|
||||||
integer epoch (in seconds)
|
integer epoch (in seconds) or float (in fractional seconds)
|
||||||
follow (bool): Follow log output. Default ``False``
|
follow (bool): Follow log output. Default ``False``
|
||||||
until (datetime or int): Show logs that occurred before the given
|
until (datetime, int, or float): Show logs that occurred before
|
||||||
datetime or integer epoch (in seconds)
|
the given datetime, integer epoch (in seconds), or
|
||||||
|
float (in fractional seconds)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(generator or str)
|
(generator of bytes or bytes)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -845,10 +866,12 @@ class ContainerApiMixin(object):
|
||||||
params['since'] = utils.datetime_to_timestamp(since)
|
params['since'] = utils.datetime_to_timestamp(since)
|
||||||
elif (isinstance(since, int) and since > 0):
|
elif (isinstance(since, int) and since > 0):
|
||||||
params['since'] = since
|
params['since'] = since
|
||||||
|
elif (isinstance(since, float) and since > 0.0):
|
||||||
|
params['since'] = since
|
||||||
else:
|
else:
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'since value should be datetime or positive int, '
|
'since value should be datetime or positive int/float,'
|
||||||
'not {}'.format(type(since))
|
f' not {type(since)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
if until is not None:
|
if until is not None:
|
||||||
|
@ -860,10 +883,12 @@ class ContainerApiMixin(object):
|
||||||
params['until'] = utils.datetime_to_timestamp(until)
|
params['until'] = utils.datetime_to_timestamp(until)
|
||||||
elif (isinstance(until, int) and until > 0):
|
elif (isinstance(until, int) and until > 0):
|
||||||
params['until'] = until
|
params['until'] = until
|
||||||
|
elif (isinstance(until, float) and until > 0.0):
|
||||||
|
params['until'] = until
|
||||||
else:
|
else:
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'until value should be datetime or positive int, '
|
f'until value should be datetime or positive int/float, '
|
||||||
'not {}'.format(type(until))
|
f'not {type(until)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
url = self._url("/containers/{0}/logs", container)
|
url = self._url("/containers/{0}/logs", container)
|
||||||
|
@ -935,7 +960,7 @@ class ContainerApiMixin(object):
|
||||||
return port_settings.get(private_port)
|
return port_settings.get(private_port)
|
||||||
|
|
||||||
for protocol in ['tcp', 'udp', 'sctp']:
|
for protocol in ['tcp', 'udp', 'sctp']:
|
||||||
h_ports = port_settings.get(private_port + '/' + protocol)
|
h_ports = port_settings.get(f"{private_port}/{protocol}")
|
||||||
if h_ports:
|
if h_ports:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -951,7 +976,7 @@ class ContainerApiMixin(object):
|
||||||
container (str): The container where the file(s) will be extracted
|
container (str): The container where the file(s) will be extracted
|
||||||
path (str): Path inside the container where the file(s) will be
|
path (str): Path inside the container where the file(s) will be
|
||||||
extracted. Must exist.
|
extracted. Must exist.
|
||||||
data (bytes): tar data to be extracted
|
data (bytes or stream): tar data to be extracted
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(bool): True if the call succeeds.
|
(bool): True if the call succeeds.
|
||||||
|
@ -1111,7 +1136,7 @@ class ContainerApiMixin(object):
|
||||||
self._raise_for_status(res)
|
self._raise_for_status(res)
|
||||||
|
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def stats(self, container, decode=None, stream=True):
|
def stats(self, container, decode=None, stream=True, one_shot=None):
|
||||||
"""
|
"""
|
||||||
Stream statistics for a specific container. Similar to the
|
Stream statistics for a specific container. Similar to the
|
||||||
``docker stats`` command.
|
``docker stats`` command.
|
||||||
|
@ -1123,6 +1148,9 @@ class ContainerApiMixin(object):
|
||||||
False by default.
|
False by default.
|
||||||
stream (bool): If set to false, only the current stats will be
|
stream (bool): If set to false, only the current stats will be
|
||||||
returned instead of a stream. True by default.
|
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:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -1130,16 +1158,30 @@ class ContainerApiMixin(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
url = self._url("/containers/{0}/stats", container)
|
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 stream:
|
||||||
return self._stream_helper(self._get(url, stream=True),
|
if one_shot:
|
||||||
decode=decode)
|
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
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if decode:
|
if decode:
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
"decode is only available in conjunction with stream=True"
|
"decode is only available in conjunction with stream=True"
|
||||||
)
|
)
|
||||||
return self._result(self._get(url, params={'stream': False}),
|
return self._result(self._get(url, params=params), json=True)
|
||||||
json=True)
|
|
||||||
|
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def stop(self, container, timeout=None):
|
def stop(self, container, timeout=None):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from datetime import datetime
|
||||||
from .. import auth, types, utils
|
from .. import auth, types, utils
|
||||||
|
|
||||||
|
|
||||||
class DaemonApiMixin(object):
|
class DaemonApiMixin:
|
||||||
@utils.minimum_version('1.25')
|
@utils.minimum_version('1.25')
|
||||||
def df(self):
|
def df(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import six
|
from .. import errors, utils
|
||||||
|
from ..types import CancellableStream
|
||||||
from .. import errors
|
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
|
|
||||||
class ExecApiMixin(object):
|
class ExecApiMixin:
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def exec_create(self, container, cmd, stdout=True, stderr=True,
|
def exec_create(self, container, cmd, stdout=True, stderr=True,
|
||||||
stdin=False, tty=False, privileged=False, user='',
|
stdin=False, tty=False, privileged=False, user='',
|
||||||
|
@ -45,7 +43,7 @@ class ExecApiMixin(object):
|
||||||
'Setting environment for exec is not supported in API < 1.25'
|
'Setting environment for exec is not supported in API < 1.25'
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(cmd, six.string_types):
|
if isinstance(cmd, str):
|
||||||
cmd = utils.split_command(cmd)
|
cmd = utils.split_command(cmd)
|
||||||
|
|
||||||
if isinstance(environment, dict):
|
if isinstance(environment, dict):
|
||||||
|
@ -127,9 +125,10 @@ class ExecApiMixin(object):
|
||||||
detach (bool): If true, detach from the exec command.
|
detach (bool): If true, detach from the exec command.
|
||||||
Default: False
|
Default: False
|
||||||
tty (bool): Allocate a pseudo-TTY. Default: False
|
tty (bool): Allocate a pseudo-TTY. Default: False
|
||||||
stream (bool): Stream response data. Default: False
|
stream (bool): Return response data progressively as an iterator
|
||||||
|
of strings, rather than a single string.
|
||||||
socket (bool): Return the connection socket to allow custom
|
socket (bool): Return the connection socket to allow custom
|
||||||
read/write operations.
|
read/write operations. Must be closed by the caller when done.
|
||||||
demux (bool): Return stdout and stderr separately
|
demux (bool): Return stdout and stderr separately
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -163,7 +162,15 @@ class ExecApiMixin(object):
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
if detach:
|
if detach:
|
||||||
return self._result(res)
|
try:
|
||||||
|
return self._result(res)
|
||||||
|
finally:
|
||||||
|
res.close()
|
||||||
if socket:
|
if socket:
|
||||||
return self._get_raw_response_socket(res)
|
return self._get_raw_response_socket(res)
|
||||||
return self._read_from_socket(res, stream, tty=tty, demux=demux)
|
|
||||||
|
output = self._read_from_socket(res, stream, tty=tty, demux=demux)
|
||||||
|
if stream:
|
||||||
|
return CancellableStream(output, res)
|
||||||
|
else:
|
||||||
|
return output
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .. import auth, errors, utils
|
from .. import auth, errors, utils
|
||||||
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ImageApiMixin(object):
|
class ImageApiMixin:
|
||||||
|
|
||||||
@utils.check_resource('image')
|
@utils.check_resource('image')
|
||||||
def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
|
def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
|
||||||
|
@ -49,7 +47,7 @@ class ImageApiMixin(object):
|
||||||
image (str): The image to show history for
|
image (str): The image to show history for
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(str): The history of the image
|
(list): The history of the image
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -130,7 +128,7 @@ class ImageApiMixin(object):
|
||||||
|
|
||||||
params = _import_image_params(
|
params = _import_image_params(
|
||||||
repository, tag, image,
|
repository, tag, image,
|
||||||
src=(src if isinstance(src, six.string_types) else None),
|
src=(src if isinstance(src, str) else None),
|
||||||
changes=changes
|
changes=changes
|
||||||
)
|
)
|
||||||
headers = {'Content-Type': 'application/tar'}
|
headers = {'Content-Type': 'application/tar'}
|
||||||
|
@ -139,7 +137,7 @@ class ImageApiMixin(object):
|
||||||
return self._result(
|
return self._result(
|
||||||
self._post(u, data=None, params=params)
|
self._post(u, data=None, params=params)
|
||||||
)
|
)
|
||||||
elif isinstance(src, six.string_types): # from file path
|
elif isinstance(src, str): # from file path
|
||||||
with open(src, 'rb') as f:
|
with open(src, 'rb') as f:
|
||||||
return self._result(
|
return self._result(
|
||||||
self._post(
|
self._post(
|
||||||
|
@ -379,7 +377,8 @@ class ImageApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> for line in client.api.pull('busybox', stream=True, decode=True):
|
>>> resp = client.api.pull('busybox', stream=True, decode=True)
|
||||||
|
... for line in resp:
|
||||||
... print(json.dumps(line, indent=4))
|
... print(json.dumps(line, indent=4))
|
||||||
{
|
{
|
||||||
"status": "Pulling image (latest) from busybox",
|
"status": "Pulling image (latest) from busybox",
|
||||||
|
@ -458,7 +457,12 @@ class ImageApiMixin(object):
|
||||||
If the server returns an error.
|
If the server returns an error.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> for line in client.api.push('yourname/app', stream=True, decode=True):
|
>>> resp = client.api.push(
|
||||||
|
... 'yourname/app',
|
||||||
|
... stream=True,
|
||||||
|
... decode=True,
|
||||||
|
... )
|
||||||
|
... for line in resp:
|
||||||
... print(line)
|
... print(line)
|
||||||
{'status': 'Pushing repository yourname/app (1 tags)'}
|
{'status': 'Pushing repository yourname/app (1 tags)'}
|
||||||
{'status': 'Pushing','progressDetail': {}, 'id': '511136ea3c5a'}
|
{'status': 'Pushing','progressDetail': {}, 'id': '511136ea3c5a'}
|
||||||
|
@ -571,7 +575,7 @@ class ImageApiMixin(object):
|
||||||
def is_file(src):
|
def is_file(src):
|
||||||
try:
|
try:
|
||||||
return (
|
return (
|
||||||
isinstance(src, six.string_types) and
|
isinstance(src, str) and
|
||||||
os.path.isfile(src)
|
os.path.isfile(src)
|
||||||
)
|
)
|
||||||
except TypeError: # a data string will make isfile() raise a TypeError
|
except TypeError: # a data string will make isfile() raise a TypeError
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from ..errors import InvalidVersion
|
|
||||||
from ..utils import check_resource, minimum_version
|
|
||||||
from ..utils import version_lt
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
|
from ..errors import InvalidVersion
|
||||||
|
from ..utils import check_resource, minimum_version, version_lt
|
||||||
|
|
||||||
|
|
||||||
class NetworkApiMixin(object):
|
class NetworkApiMixin:
|
||||||
def networks(self, names=None, ids=None, filters=None):
|
def networks(self, names=None, ids=None, filters=None):
|
||||||
"""
|
"""
|
||||||
List networks. Similar to the ``docker network ls`` command.
|
List networks. Similar to the ``docker network ls`` command.
|
||||||
|
@ -75,7 +74,7 @@ class NetworkApiMixin(object):
|
||||||
Example:
|
Example:
|
||||||
A network using the bridge driver:
|
A network using the bridge driver:
|
||||||
|
|
||||||
>>> client.create_network("network1", driver="bridge")
|
>>> client.api.create_network("network1", driver="bridge")
|
||||||
|
|
||||||
You can also create more advanced networks with custom IPAM
|
You can also create more advanced networks with custom IPAM
|
||||||
configurations. For example, setting the subnet to
|
configurations. For example, setting the subnet to
|
||||||
|
@ -90,7 +89,7 @@ class NetworkApiMixin(object):
|
||||||
>>> ipam_config = docker.types.IPAMConfig(
|
>>> ipam_config = docker.types.IPAMConfig(
|
||||||
pool_configs=[ipam_pool]
|
pool_configs=[ipam_pool]
|
||||||
)
|
)
|
||||||
>>> docker_client.create_network("network1", driver="bridge",
|
>>> client.api.create_network("network1", driver="bridge",
|
||||||
ipam=ipam_config)
|
ipam=ipam_config)
|
||||||
"""
|
"""
|
||||||
if options is not None and not isinstance(options, dict):
|
if options is not None and not isinstance(options, dict):
|
||||||
|
@ -216,7 +215,8 @@ class NetworkApiMixin(object):
|
||||||
def connect_container_to_network(self, container, net_id,
|
def connect_container_to_network(self, container, net_id,
|
||||||
ipv4_address=None, ipv6_address=None,
|
ipv4_address=None, ipv6_address=None,
|
||||||
aliases=None, links=None,
|
aliases=None, links=None,
|
||||||
link_local_ips=None, driver_opt=None):
|
link_local_ips=None, driver_opt=None,
|
||||||
|
mac_address=None):
|
||||||
"""
|
"""
|
||||||
Connect a container to a network.
|
Connect a container to a network.
|
||||||
|
|
||||||
|
@ -235,13 +235,16 @@ class NetworkApiMixin(object):
|
||||||
network, using the IPv6 protocol. Defaults to ``None``.
|
network, using the IPv6 protocol. Defaults to ``None``.
|
||||||
link_local_ips (:py:class:`list`): A list of link-local
|
link_local_ips (:py:class:`list`): A list of link-local
|
||||||
(IPv4/IPv6) addresses.
|
(IPv4/IPv6) addresses.
|
||||||
|
mac_address (str): The MAC address of this container on the
|
||||||
|
network. Defaults to ``None``.
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"Container": container,
|
"Container": container,
|
||||||
"EndpointConfig": self.create_endpoint_config(
|
"EndpointConfig": self.create_endpoint_config(
|
||||||
aliases=aliases, links=links, ipv4_address=ipv4_address,
|
aliases=aliases, links=links, ipv4_address=ipv4_address,
|
||||||
ipv6_address=ipv6_address, link_local_ips=link_local_ips,
|
ipv6_address=ipv6_address, link_local_ips=link_local_ips,
|
||||||
driver_opt=driver_opt
|
driver_opt=driver_opt,
|
||||||
|
mac_address=mac_address
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import six
|
|
||||||
|
|
||||||
from .. import auth, utils
|
from .. import auth, utils
|
||||||
|
|
||||||
|
|
||||||
class PluginApiMixin(object):
|
class PluginApiMixin:
|
||||||
@utils.minimum_version('1.25')
|
@utils.minimum_version('1.25')
|
||||||
@utils.check_resource('name')
|
@utils.check_resource('name')
|
||||||
def configure_plugin(self, name, options):
|
def configure_plugin(self, name, options):
|
||||||
|
@ -21,7 +19,7 @@ class PluginApiMixin(object):
|
||||||
url = self._url('/plugins/{0}/set', name)
|
url = self._url('/plugins/{0}/set', name)
|
||||||
data = options
|
data = options
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data = ['{0}={1}'.format(k, v) for k, v in six.iteritems(data)]
|
data = [f'{k}={v}' for k, v in data.items()]
|
||||||
res = self._post_json(url, data=data)
|
res = self._post_json(url, data=data)
|
||||||
self._raise_for_status(res)
|
self._raise_for_status(res)
|
||||||
return True
|
return True
|
||||||
|
@ -53,19 +51,20 @@ class PluginApiMixin(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@utils.minimum_version('1.25')
|
@utils.minimum_version('1.25')
|
||||||
def disable_plugin(self, name):
|
def disable_plugin(self, name, force=False):
|
||||||
"""
|
"""
|
||||||
Disable an installed plugin.
|
Disable an installed plugin.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (string): The name of the plugin. The ``:latest`` tag is
|
name (string): The name of the plugin. The ``:latest`` tag is
|
||||||
optional, and is the default if omitted.
|
optional, and is the default if omitted.
|
||||||
|
force (bool): To enable the force query parameter.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``True`` if successful
|
``True`` if successful
|
||||||
"""
|
"""
|
||||||
url = self._url('/plugins/{0}/disable', name)
|
url = self._url('/plugins/{0}/disable', name)
|
||||||
res = self._post(url)
|
res = self._post(url, params={'force': force})
|
||||||
self._raise_for_status(res)
|
self._raise_for_status(res)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import six
|
from .. import errors, utils
|
||||||
|
|
||||||
from .. import errors
|
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
|
|
||||||
class SecretApiMixin(object):
|
class SecretApiMixin:
|
||||||
@utils.minimum_version('1.25')
|
@utils.minimum_version('1.25')
|
||||||
def create_secret(self, name, data, labels=None, driver=None):
|
def create_secret(self, name, data, labels=None, driver=None):
|
||||||
"""
|
"""
|
||||||
|
@ -25,8 +22,7 @@ class SecretApiMixin(object):
|
||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
data = base64.b64encode(data)
|
data = base64.b64encode(data)
|
||||||
if six.PY3:
|
data = data.decode('ascii')
|
||||||
data = data.decode('ascii')
|
|
||||||
body = {
|
body = {
|
||||||
'Data': data,
|
'Data': data,
|
||||||
'Name': name,
|
'Name': name,
|
||||||
|
|
|
@ -7,9 +7,7 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
|
||||||
|
|
||||||
def raise_version_error(param, min_version):
|
def raise_version_error(param, min_version):
|
||||||
raise errors.InvalidVersion(
|
raise errors.InvalidVersion(
|
||||||
'{} is not supported in API version < {}'.format(
|
f'{param} is not supported in API version < {min_version}'
|
||||||
param, min_version
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if update_config is not None:
|
if update_config is not None:
|
||||||
|
@ -45,7 +43,7 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
|
||||||
if task_template is not None:
|
if task_template is not None:
|
||||||
if 'ForceUpdate' in task_template and utils.version_lt(
|
if 'ForceUpdate' in task_template and utils.version_lt(
|
||||||
version, '1.25'):
|
version, '1.25'):
|
||||||
raise_version_error('force_update', '1.25')
|
raise_version_error('force_update', '1.25')
|
||||||
|
|
||||||
if task_template.get('Placement'):
|
if task_template.get('Placement'):
|
||||||
if utils.version_lt(version, '1.30'):
|
if utils.version_lt(version, '1.30'):
|
||||||
|
@ -113,7 +111,7 @@ def _merge_task_template(current, override):
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
class ServiceApiMixin(object):
|
class ServiceApiMixin:
|
||||||
@utils.minimum_version('1.24')
|
@utils.minimum_version('1.24')
|
||||||
def create_service(
|
def create_service(
|
||||||
self, task_template, name=None, labels=None, mode=None,
|
self, task_template, name=None, labels=None, mode=None,
|
||||||
|
@ -262,7 +260,7 @@ class ServiceApiMixin(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@utils.minimum_version('1.24')
|
@utils.minimum_version('1.24')
|
||||||
def services(self, filters=None):
|
def services(self, filters=None, status=None):
|
||||||
"""
|
"""
|
||||||
List services.
|
List services.
|
||||||
|
|
||||||
|
@ -270,6 +268,8 @@ class ServiceApiMixin(object):
|
||||||
filters (dict): Filters to process on the nodes list. Valid
|
filters (dict): Filters to process on the nodes list. Valid
|
||||||
filters: ``id``, ``name`` , ``label`` and ``mode``.
|
filters: ``id``, ``name`` , ``label`` and ``mode``.
|
||||||
Default: ``None``.
|
Default: ``None``.
|
||||||
|
status (bool): Include the service task count of running and
|
||||||
|
desired tasks. Default: ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of dictionaries containing data about each service.
|
A list of dictionaries containing data about each service.
|
||||||
|
@ -281,6 +281,12 @@ class ServiceApiMixin(object):
|
||||||
params = {
|
params = {
|
||||||
'filters': utils.convert_filters(filters) if filters else None
|
'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')
|
url = self._url('/services')
|
||||||
return self._result(self._get(url, params=params), True)
|
return self._result(self._get(url, params=params), True)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import http.client as http_client
|
||||||
import logging
|
import logging
|
||||||
from six.moves import http_client
|
|
||||||
|
from .. import errors, types, utils
|
||||||
from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
|
from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
|
||||||
from .. import errors
|
|
||||||
from .. import types
|
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SwarmApiMixin(object):
|
class SwarmApiMixin:
|
||||||
|
|
||||||
def create_swarm_spec(self, *args, **kwargs):
|
def create_swarm_spec(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -58,10 +57,10 @@ class SwarmApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> spec = client.create_swarm_spec(
|
>>> spec = client.api.create_swarm_spec(
|
||||||
snapshot_interval=5000, log_entries_for_slow_followers=1200
|
snapshot_interval=5000, log_entries_for_slow_followers=1200
|
||||||
)
|
)
|
||||||
>>> client.init_swarm(
|
>>> client.api.init_swarm(
|
||||||
advertise_addr='eth0', listen_addr='0.0.0.0:5000',
|
advertise_addr='eth0', listen_addr='0.0.0.0:5000',
|
||||||
force_new_cluster=False, swarm_spec=spec
|
force_new_cluster=False, swarm_spec=spec
|
||||||
)
|
)
|
||||||
|
@ -85,7 +84,7 @@ class SwarmApiMixin(object):
|
||||||
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
|
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
|
||||||
force_new_cluster=False, swarm_spec=None,
|
force_new_cluster=False, swarm_spec=None,
|
||||||
default_addr_pool=None, subnet_size=None,
|
default_addr_pool=None, subnet_size=None,
|
||||||
data_path_addr=None):
|
data_path_addr=None, data_path_port=None):
|
||||||
"""
|
"""
|
||||||
Initialize a new Swarm using the current connected engine as the first
|
Initialize a new Swarm using the current connected engine as the first
|
||||||
node.
|
node.
|
||||||
|
@ -118,6 +117,9 @@ class SwarmApiMixin(object):
|
||||||
networks created from the default subnet pool. Default: None
|
networks created from the default subnet pool. Default: None
|
||||||
data_path_addr (string): Address or interface to use for data path
|
data_path_addr (string): Address or interface to use for data path
|
||||||
traffic. For example, 192.168.1.1, or an interface, like eth0.
|
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:
|
Returns:
|
||||||
(str): The ID of the created node.
|
(str): The ID of the created node.
|
||||||
|
@ -166,6 +168,14 @@ class SwarmApiMixin(object):
|
||||||
)
|
)
|
||||||
data['DataPathAddr'] = data_path_addr
|
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)
|
response = self._post_json(url, data=data)
|
||||||
return self._result(response, json=True)
|
return self._result(response, json=True)
|
||||||
|
|
||||||
|
@ -354,8 +364,8 @@ class SwarmApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> key = client.get_unlock_key()
|
>>> key = client.api.get_unlock_key()
|
||||||
>>> client.unlock_node(key)
|
>>> client.unlock_swarm(key)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(key, dict):
|
if isinstance(key, dict):
|
||||||
|
@ -396,7 +406,7 @@ class SwarmApiMixin(object):
|
||||||
'Role': 'manager',
|
'Role': 'manager',
|
||||||
'Labels': {'foo': 'bar'}
|
'Labels': {'foo': 'bar'}
|
||||||
}
|
}
|
||||||
>>> client.update_node(node_id='24ifsmvkjbyhk', version=8,
|
>>> client.api.update_node(node_id='24ifsmvkjbyhk', version=8,
|
||||||
node_spec=node_spec)
|
node_spec=node_spec)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from .. import errors
|
from .. import errors, utils
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeApiMixin(object):
|
class VolumeApiMixin:
|
||||||
def volumes(self, filters=None):
|
def volumes(self, filters=None):
|
||||||
"""
|
"""
|
||||||
List volumes currently registered by the docker daemon. Similar to the
|
List volumes currently registered by the docker daemon. Similar to the
|
||||||
|
@ -21,7 +20,7 @@ class VolumeApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> cli.volumes()
|
>>> client.api.volumes()
|
||||||
{u'Volumes': [{u'Driver': u'local',
|
{u'Volumes': [{u'Driver': u'local',
|
||||||
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
||||||
u'Name': u'foobar'},
|
u'Name': u'foobar'},
|
||||||
|
@ -56,15 +55,18 @@ class VolumeApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> volume = cli.create_volume(name='foobar', driver='local',
|
>>> volume = client.api.create_volume(
|
||||||
driver_opts={'foo': 'bar', 'baz': 'false'},
|
... name='foobar',
|
||||||
labels={"key": "value"})
|
... driver='local',
|
||||||
>>> print(volume)
|
... driver_opts={'foo': 'bar', 'baz': 'false'},
|
||||||
|
... labels={"key": "value"},
|
||||||
|
... )
|
||||||
|
... print(volume)
|
||||||
{u'Driver': u'local',
|
{u'Driver': u'local',
|
||||||
u'Labels': {u'key': u'value'},
|
u'Labels': {u'key': u'value'},
|
||||||
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
||||||
u'Name': u'foobar',
|
u'Name': u'foobar',
|
||||||
u'Scope': u'local'}
|
u'Scope': u'local'}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
url = self._url('/volumes/create')
|
url = self._url('/volumes/create')
|
||||||
|
@ -104,7 +106,7 @@ class VolumeApiMixin(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> cli.inspect_volume('foobar')
|
>>> client.api.inspect_volume('foobar')
|
||||||
{u'Driver': u'local',
|
{u'Driver': u'local',
|
||||||
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data',
|
||||||
u'Name': u'foobar'}
|
u'Name': u'foobar'}
|
||||||
|
|
|
@ -2,14 +2,11 @@ import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import six
|
from . import credentials, errors
|
||||||
|
|
||||||
from . import credentials
|
|
||||||
from . import errors
|
|
||||||
from .utils import config
|
from .utils import config
|
||||||
|
|
||||||
INDEX_NAME = 'docker.io'
|
INDEX_NAME = 'docker.io'
|
||||||
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME)
|
INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
|
||||||
TOKEN_USERNAME = '<token>'
|
TOKEN_USERNAME = '<token>'
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -18,21 +15,21 @@ log = logging.getLogger(__name__)
|
||||||
def resolve_repository_name(repo_name):
|
def resolve_repository_name(repo_name):
|
||||||
if '://' in repo_name:
|
if '://' in repo_name:
|
||||||
raise errors.InvalidRepository(
|
raise errors.InvalidRepository(
|
||||||
'Repository name cannot contain a scheme ({0})'.format(repo_name)
|
f'Repository name cannot contain a scheme ({repo_name})'
|
||||||
)
|
)
|
||||||
|
|
||||||
index_name, remote_name = split_repo_name(repo_name)
|
index_name, remote_name = split_repo_name(repo_name)
|
||||||
if index_name[0] == '-' or index_name[-1] == '-':
|
if index_name[0] == '-' or index_name[-1] == '-':
|
||||||
raise errors.InvalidRepository(
|
raise errors.InvalidRepository(
|
||||||
'Invalid index name ({0}). Cannot begin or end with a'
|
f'Invalid index name ({index_name}). '
|
||||||
' hyphen.'.format(index_name)
|
'Cannot begin or end with a hyphen.'
|
||||||
)
|
)
|
||||||
return resolve_index_name(index_name), remote_name
|
return resolve_index_name(index_name), remote_name
|
||||||
|
|
||||||
|
|
||||||
def resolve_index_name(index_name):
|
def resolve_index_name(index_name):
|
||||||
index_name = convert_to_hostname(index_name)
|
index_name = convert_to_hostname(index_name)
|
||||||
if index_name == 'index.' + INDEX_NAME:
|
if index_name == f"index.{INDEX_NAME}":
|
||||||
index_name = INDEX_NAME
|
index_name = INDEX_NAME
|
||||||
return index_name
|
return index_name
|
||||||
|
|
||||||
|
@ -98,12 +95,10 @@ class AuthConfig(dict):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conf = {}
|
conf = {}
|
||||||
for registry, entry in six.iteritems(entries):
|
for registry, entry in entries.items():
|
||||||
if not isinstance(entry, dict):
|
if not isinstance(entry, dict):
|
||||||
log.debug(
|
log.debug(
|
||||||
'Config entry for key {0} is not auth config'.format(
|
f'Config entry for key {registry} is not auth config'
|
||||||
registry
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# We sometimes fall back to parsing the whole config as if it
|
# We sometimes fall back to parsing the whole config as if it
|
||||||
# was the auth config by itself, for legacy purposes. In that
|
# was the auth config by itself, for legacy purposes. In that
|
||||||
|
@ -111,17 +106,11 @@ class AuthConfig(dict):
|
||||||
# keys is not formatted properly.
|
# keys is not formatted properly.
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise errors.InvalidConfigFile(
|
raise errors.InvalidConfigFile(
|
||||||
'Invalid configuration for registry {0}'.format(
|
f'Invalid configuration for registry {registry}'
|
||||||
registry
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
if 'identitytoken' in entry:
|
if 'identitytoken' in entry:
|
||||||
log.debug(
|
log.debug(f'Found an IdentityToken entry for registry {registry}')
|
||||||
'Found an IdentityToken entry for registry {0}'.format(
|
|
||||||
registry
|
|
||||||
)
|
|
||||||
)
|
|
||||||
conf[registry] = {
|
conf[registry] = {
|
||||||
'IdentityToken': entry['identitytoken']
|
'IdentityToken': entry['identitytoken']
|
||||||
}
|
}
|
||||||
|
@ -132,16 +121,15 @@ class AuthConfig(dict):
|
||||||
# a valid value in the auths config.
|
# a valid value in the auths config.
|
||||||
# https://github.com/docker/compose/issues/3265
|
# https://github.com/docker/compose/issues/3265
|
||||||
log.debug(
|
log.debug(
|
||||||
'Auth data for {0} is absent. Client might be using a '
|
f'Auth data for {registry} is absent. '
|
||||||
'credentials store instead.'.format(registry)
|
f'Client might be using a credentials store instead.'
|
||||||
)
|
)
|
||||||
conf[registry] = {}
|
conf[registry] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
username, password = decode_auth(entry['auth'])
|
username, password = decode_auth(entry['auth'])
|
||||||
log.debug(
|
log.debug(
|
||||||
'Found entry (registry={0}, username={1})'
|
f'Found entry (registry={registry!r}, username={username!r})'
|
||||||
.format(repr(registry), repr(username))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
conf[registry] = {
|
conf[registry] = {
|
||||||
|
@ -170,7 +158,7 @@ class AuthConfig(dict):
|
||||||
try:
|
try:
|
||||||
with open(config_file) as f:
|
with open(config_file) as f:
|
||||||
config_dict = json.load(f)
|
config_dict = json.load(f)
|
||||||
except (IOError, KeyError, ValueError) as e:
|
except (OSError, KeyError, ValueError) as e:
|
||||||
# Likely missing new Docker config file or it's in an
|
# Likely missing new Docker config file or it's in an
|
||||||
# unknown format, continue to attempt to read old location
|
# unknown format, continue to attempt to read old location
|
||||||
# and format.
|
# and format.
|
||||||
|
@ -230,7 +218,7 @@ class AuthConfig(dict):
|
||||||
store_name = self.get_credential_store(registry)
|
store_name = self.get_credential_store(registry)
|
||||||
if store_name is not None:
|
if store_name is not None:
|
||||||
log.debug(
|
log.debug(
|
||||||
'Using credentials store "{0}"'.format(store_name)
|
f'Using credentials store "{store_name}"'
|
||||||
)
|
)
|
||||||
cfg = self._resolve_authconfig_credstore(registry, store_name)
|
cfg = self._resolve_authconfig_credstore(registry, store_name)
|
||||||
if cfg is not None:
|
if cfg is not None:
|
||||||
|
@ -239,15 +227,15 @@ class AuthConfig(dict):
|
||||||
|
|
||||||
# Default to the public index server
|
# Default to the public index server
|
||||||
registry = resolve_index_name(registry) if registry else INDEX_NAME
|
registry = resolve_index_name(registry) if registry else INDEX_NAME
|
||||||
log.debug("Looking for auth entry for {0}".format(repr(registry)))
|
log.debug(f"Looking for auth entry for {repr(registry)}")
|
||||||
|
|
||||||
if registry in self.auths:
|
if registry in self.auths:
|
||||||
log.debug("Found {0}".format(repr(registry)))
|
log.debug(f"Found {repr(registry)}")
|
||||||
return self.auths[registry]
|
return self.auths[registry]
|
||||||
|
|
||||||
for key, conf in six.iteritems(self.auths):
|
for key, conf in self.auths.items():
|
||||||
if resolve_index_name(key) == registry:
|
if resolve_index_name(key) == registry:
|
||||||
log.debug("Found {0}".format(repr(key)))
|
log.debug(f"Found {repr(key)}")
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
log.debug("No entry found")
|
log.debug("No entry found")
|
||||||
|
@ -258,7 +246,7 @@ class AuthConfig(dict):
|
||||||
# The ecosystem is a little schizophrenic with index.docker.io VS
|
# The ecosystem is a little schizophrenic with index.docker.io VS
|
||||||
# docker.io - in that case, it seems the full URL is necessary.
|
# docker.io - in that case, it seems the full URL is necessary.
|
||||||
registry = INDEX_URL
|
registry = INDEX_URL
|
||||||
log.debug("Looking for auth entry for {0}".format(repr(registry)))
|
log.debug(f"Looking for auth entry for {repr(registry)}")
|
||||||
store = self._get_store_instance(credstore_name)
|
store = self._get_store_instance(credstore_name)
|
||||||
try:
|
try:
|
||||||
data = store.get(registry)
|
data = store.get(registry)
|
||||||
|
@ -278,8 +266,8 @@ class AuthConfig(dict):
|
||||||
return None
|
return None
|
||||||
except credentials.StoreError as e:
|
except credentials.StoreError as e:
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Credentials store error: {0}'.format(repr(e))
|
f'Credentials store error: {repr(e)}'
|
||||||
)
|
) from e
|
||||||
|
|
||||||
def _get_store_instance(self, name):
|
def _get_store_instance(self, name):
|
||||||
if name not in self._stores:
|
if name not in self._stores:
|
||||||
|
@ -329,7 +317,7 @@ def convert_to_hostname(url):
|
||||||
|
|
||||||
|
|
||||||
def decode_auth(auth):
|
def decode_auth(auth):
|
||||||
if isinstance(auth, six.string_types):
|
if isinstance(auth, str):
|
||||||
auth = auth.encode('ascii')
|
auth = auth.encode('ascii')
|
||||||
s = base64.b64decode(auth)
|
s = base64.b64decode(auth)
|
||||||
login, pwd = s.split(b':', 1)
|
login, pwd = s.split(b':', 1)
|
||||||
|
@ -385,7 +373,6 @@ def _load_legacy_config(config_file):
|
||||||
}}
|
}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
pass
|
|
||||||
|
|
||||||
log.debug("All parsing attempts failed - returning empty config")
|
log.debug("All parsing attempts failed - returning empty config")
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from .api.client import APIClient
|
from .api.client import APIClient
|
||||||
from .constants import (DEFAULT_TIMEOUT_SECONDS, DEFAULT_MAX_POOL_SIZE)
|
from .constants import DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS
|
||||||
from .models.configs import ConfigCollection
|
from .models.configs import ConfigCollection
|
||||||
from .models.containers import ContainerCollection
|
from .models.containers import ContainerCollection
|
||||||
from .models.images import ImageCollection
|
from .models.images import ImageCollection
|
||||||
|
@ -13,7 +13,7 @@ from .models.volumes import VolumeCollection
|
||||||
from .utils import kwargs_from_env
|
from .utils import kwargs_from_env
|
||||||
|
|
||||||
|
|
||||||
class DockerClient(object):
|
class DockerClient:
|
||||||
"""
|
"""
|
||||||
A client for communicating with a Docker server.
|
A client for communicating with a Docker server.
|
||||||
|
|
||||||
|
@ -71,8 +71,6 @@ class DockerClient(object):
|
||||||
timeout (int): Default timeout for API calls, in seconds.
|
timeout (int): Default timeout for API calls, in seconds.
|
||||||
max_pool_size (int): The maximum number of connections
|
max_pool_size (int): The maximum number of connections
|
||||||
to save in the pool.
|
to save in the pool.
|
||||||
ssl_version (int): A valid `SSL version`_.
|
|
||||||
assert_hostname (bool): Verify the hostname of the server.
|
|
||||||
environment (dict): The environment to read environment variables
|
environment (dict): The environment to read environment variables
|
||||||
from. Default: the value of ``os.environ``
|
from. Default: the value of ``os.environ``
|
||||||
credstore_env (dict): Override environment variables when calling
|
credstore_env (dict): Override environment variables when calling
|
||||||
|
@ -212,7 +210,7 @@ class DockerClient(object):
|
||||||
close.__doc__ = APIClient.close.__doc__
|
close.__doc__ = APIClient.close.__doc__
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
s = ["'DockerClient' object has no attribute '{}'".format(name)]
|
s = [f"'DockerClient' object has no attribute '{name}'"]
|
||||||
# If a user calls a method on APIClient, they
|
# If a user calls a method on APIClient, they
|
||||||
if hasattr(APIClient, name):
|
if hasattr(APIClient, name):
|
||||||
s.append("In Docker SDK for Python 2.0, this method is now on the "
|
s.append("In Docker SDK for Python 2.0, this method is now on the "
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import sys
|
import sys
|
||||||
from .version import version
|
|
||||||
|
|
||||||
DEFAULT_DOCKER_API_VERSION = '1.39'
|
from .version import __version__
|
||||||
MINIMUM_DOCKER_API_VERSION = '1.21'
|
|
||||||
|
DEFAULT_DOCKER_API_VERSION = '1.45'
|
||||||
|
MINIMUM_DOCKER_API_VERSION = '1.24'
|
||||||
DEFAULT_TIMEOUT_SECONDS = 60
|
DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
STREAM_HEADER_SIZE_BYTES = 8
|
STREAM_HEADER_SIZE_BYTES = 8
|
||||||
CONTAINER_LIMITS_KEYS = [
|
CONTAINER_LIMITS_KEYS = [
|
||||||
|
@ -28,7 +29,7 @@ INSECURE_REGISTRY_DEPRECATION_WARNING = \
|
||||||
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
|
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
|
||||||
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
|
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
|
||||||
|
|
||||||
DEFAULT_USER_AGENT = "docker-sdk-python/{0}".format(version)
|
DEFAULT_USER_AGENT = f"docker-sdk-python/{__version__}"
|
||||||
DEFAULT_NUM_POOLS = 25
|
DEFAULT_NUM_POOLS = 25
|
||||||
|
|
||||||
# The OpenSSH server default value for MaxSessions is 10 which means we can
|
# The OpenSSH server default value for MaxSessions is 10 which means we can
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
# flake8: noqa
|
|
||||||
from .context import Context
|
|
||||||
from .api import ContextAPI
|
from .api import ContextAPI
|
||||||
|
from .context import Context
|
||||||
|
|
|
@ -2,14 +2,17 @@ import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from docker import errors
|
from docker import errors
|
||||||
from docker.context.config import get_meta_dir
|
|
||||||
from docker.context.config import METAFILE
|
from .config import (
|
||||||
from docker.context.config import get_current_context_name
|
METAFILE,
|
||||||
from docker.context.config import write_context_name_to_docker_config
|
get_current_context_name,
|
||||||
from docker.context import Context
|
get_meta_dir,
|
||||||
|
write_context_name_to_docker_config,
|
||||||
|
)
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
|
|
||||||
class ContextAPI(object):
|
class ContextAPI:
|
||||||
"""Context API.
|
"""Context API.
|
||||||
Contains methods for context management:
|
Contains methods for context management:
|
||||||
create, list, remove, get, inspect.
|
create, list, remove, get, inspect.
|
||||||
|
@ -109,12 +112,12 @@ class ContextAPI(object):
|
||||||
if filename == METAFILE:
|
if filename == METAFILE:
|
||||||
try:
|
try:
|
||||||
data = json.load(
|
data = json.load(
|
||||||
open(os.path.join(dirname, filename), "r"))
|
open(os.path.join(dirname, filename)))
|
||||||
names.append(data["Name"])
|
names.append(data["Name"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise errors.ContextException(
|
raise errors.ContextException(
|
||||||
"Failed to load metafile {}: {}".format(
|
f"Failed to load metafile {filename}: {e}",
|
||||||
filename, e))
|
) from e
|
||||||
|
|
||||||
contexts = [cls.DEFAULT_CONTEXT]
|
contexts = [cls.DEFAULT_CONTEXT]
|
||||||
for name in names:
|
for name in names:
|
||||||
|
@ -138,7 +141,7 @@ class ContextAPI(object):
|
||||||
err = write_context_name_to_docker_config(name)
|
err = write_context_name_to_docker_config(name)
|
||||||
if err:
|
if err:
|
||||||
raise errors.ContextException(
|
raise errors.ContextException(
|
||||||
'Failed to set current context: {}'.format(err))
|
f'Failed to set current context: {err}')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_context(cls, name):
|
def remove_context(cls, name):
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
from docker import utils
|
from docker import utils
|
||||||
from docker.constants import IS_WINDOWS_PLATFORM
|
from docker.constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM
|
||||||
from docker.constants import DEFAULT_UNIX_SOCKET
|
|
||||||
from docker.utils.config import find_config_file
|
from docker.utils.config import find_config_file
|
||||||
|
|
||||||
METAFILE = "meta.json"
|
METAFILE = "meta.json"
|
||||||
|
@ -15,7 +14,7 @@ def get_current_context_name():
|
||||||
docker_cfg_path = find_config_file()
|
docker_cfg_path = find_config_file()
|
||||||
if docker_cfg_path:
|
if docker_cfg_path:
|
||||||
try:
|
try:
|
||||||
with open(docker_cfg_path, "r") as f:
|
with open(docker_cfg_path) as f:
|
||||||
name = json.load(f).get("currentContext", "default")
|
name = json.load(f).get("currentContext", "default")
|
||||||
except Exception:
|
except Exception:
|
||||||
return "default"
|
return "default"
|
||||||
|
@ -29,7 +28,7 @@ def write_context_name_to_docker_config(name=None):
|
||||||
config = {}
|
config = {}
|
||||||
if docker_cfg_path:
|
if docker_cfg_path:
|
||||||
try:
|
try:
|
||||||
with open(docker_cfg_path, "r") as f:
|
with open(docker_cfg_path) as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return e
|
return e
|
||||||
|
@ -77,5 +76,6 @@ def get_context_host(path=None, tls=False):
|
||||||
host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls)
|
host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls)
|
||||||
if host == DEFAULT_UNIX_SOCKET:
|
if host == DEFAULT_UNIX_SOCKET:
|
||||||
# remove http+ from default docker socket url
|
# remove http+ from default docker socket url
|
||||||
return host.strip("http+")
|
if host.startswith("http+"):
|
||||||
|
host = host[5:]
|
||||||
return host
|
return host
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from shutil import copyfile, rmtree
|
from shutil import copyfile, rmtree
|
||||||
from docker.tls import TLSConfig
|
|
||||||
from docker.errors import ContextException
|
from docker.errors import ContextException
|
||||||
from docker.context.config import get_meta_dir
|
from docker.tls import TLSConfig
|
||||||
from docker.context.config import get_meta_file
|
|
||||||
from docker.context.config import get_tls_dir
|
from .config import (
|
||||||
from docker.context.config import get_context_host
|
get_context_host,
|
||||||
|
get_meta_dir,
|
||||||
|
get_meta_file,
|
||||||
|
get_tls_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
|
@ -42,8 +46,9 @@ class Context:
|
||||||
for k, v in endpoints.items():
|
for k, v in endpoints.items():
|
||||||
if not isinstance(v, dict):
|
if not isinstance(v, dict):
|
||||||
# unknown format
|
# unknown format
|
||||||
raise ContextException("""Unknown endpoint format for
|
raise ContextException(
|
||||||
context {}: {}""".format(name, v))
|
f"Unknown endpoint format for context {name}: {v}",
|
||||||
|
)
|
||||||
|
|
||||||
self.endpoints[k] = v
|
self.endpoints[k] = v
|
||||||
if k != "docker":
|
if k != "docker":
|
||||||
|
@ -94,10 +99,11 @@ class Context:
|
||||||
try:
|
try:
|
||||||
with open(meta_file) as f:
|
with open(meta_file) as f:
|
||||||
metadata = json.load(f)
|
metadata = json.load(f)
|
||||||
except (IOError, KeyError, ValueError) as e:
|
except (OSError, KeyError, ValueError) as e:
|
||||||
# unknown format
|
# unknown format
|
||||||
raise Exception("""Detected corrupted meta file for
|
raise Exception(
|
||||||
context {} : {}""".format(name, e))
|
f"Detected corrupted meta file for context {name} : {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
# for docker endpoints, set defaults for
|
# for docker endpoints, set defaults for
|
||||||
# Host and SkipTLSVerify fields
|
# Host and SkipTLSVerify fields
|
||||||
|
@ -171,7 +177,7 @@ class Context:
|
||||||
rmtree(self.tls_path)
|
rmtree(self.tls_path)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
return f"<{self.__class__.__name__}: '{self.name}'>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return json.dumps(self.__call__(), indent=2)
|
return json.dumps(self.__call__(), indent=2)
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
# flake8: noqa
|
from .constants import (
|
||||||
|
DEFAULT_LINUX_STORE,
|
||||||
|
DEFAULT_OSX_STORE,
|
||||||
|
DEFAULT_WIN32_STORE,
|
||||||
|
PROGRAM_PREFIX,
|
||||||
|
)
|
||||||
|
from .errors import CredentialsNotFound, StoreError
|
||||||
from .store import Store
|
from .store import Store
|
||||||
from .errors import StoreError, CredentialsNotFound
|
|
||||||
from .constants import *
|
|
||||||
|
|
|
@ -13,13 +13,5 @@ class InitializationError(StoreError):
|
||||||
def process_store_error(cpe, program):
|
def process_store_error(cpe, program):
|
||||||
message = cpe.output.decode('utf-8')
|
message = cpe.output.decode('utf-8')
|
||||||
if 'credentials not found in native keychain' in message:
|
if 'credentials not found in native keychain' in message:
|
||||||
return CredentialsNotFound(
|
return CredentialsNotFound(f'No matching credentials in {program}')
|
||||||
'No matching credentials in {}'.format(
|
return StoreError(f'Credentials store {program} exited with "{message}".')
|
||||||
program
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return StoreError(
|
|
||||||
'Credentials store {} exited with "{}".'.format(
|
|
||||||
program, cpe.output.decode('utf-8').strip()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,36 +1,33 @@
|
||||||
import errno
|
import errno
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import warnings
|
||||||
|
|
||||||
import six
|
from . import constants, errors
|
||||||
|
|
||||||
from . import constants
|
|
||||||
from . import errors
|
|
||||||
from .utils import create_environment_dict
|
from .utils import create_environment_dict
|
||||||
from .utils import find_executable
|
|
||||||
|
|
||||||
|
|
||||||
class Store(object):
|
class Store:
|
||||||
def __init__(self, program, environment=None):
|
def __init__(self, program, environment=None):
|
||||||
""" Create a store object that acts as an interface to
|
""" Create a store object that acts as an interface to
|
||||||
perform the basic operations for storing, retrieving
|
perform the basic operations for storing, retrieving
|
||||||
and erasing credentials using `program`.
|
and erasing credentials using `program`.
|
||||||
"""
|
"""
|
||||||
self.program = constants.PROGRAM_PREFIX + program
|
self.program = constants.PROGRAM_PREFIX + program
|
||||||
self.exe = find_executable(self.program)
|
self.exe = shutil.which(self.program)
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
if self.exe is None:
|
if self.exe is None:
|
||||||
raise errors.InitializationError(
|
warnings.warn(
|
||||||
'{} not installed or not available in PATH'.format(
|
f'{self.program} not installed or not available in PATH',
|
||||||
self.program
|
stacklevel=1,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, server):
|
def get(self, server):
|
||||||
""" Retrieve credentials for `server`. If no credentials are found,
|
""" Retrieve credentials for `server`. If no credentials are found,
|
||||||
a `StoreError` will be raised.
|
a `StoreError` will be raised.
|
||||||
"""
|
"""
|
||||||
if not isinstance(server, six.binary_type):
|
if not isinstance(server, bytes):
|
||||||
server = server.encode('utf-8')
|
server = server.encode('utf-8')
|
||||||
data = self._execute('get', server)
|
data = self._execute('get', server)
|
||||||
result = json.loads(data.decode('utf-8'))
|
result = json.loads(data.decode('utf-8'))
|
||||||
|
@ -41,7 +38,7 @@ class Store(object):
|
||||||
# raise CredentialsNotFound
|
# raise CredentialsNotFound
|
||||||
if result['Username'] == '' and result['Secret'] == '':
|
if result['Username'] == '' and result['Secret'] == '':
|
||||||
raise errors.CredentialsNotFound(
|
raise errors.CredentialsNotFound(
|
||||||
'No matching credentials in {}'.format(self.program)
|
f'No matching credentials in {self.program}'
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -61,7 +58,7 @@ class Store(object):
|
||||||
""" Erase credentials for `server`. Raises a `StoreError` if an error
|
""" Erase credentials for `server`. Raises a `StoreError` if an error
|
||||||
occurs.
|
occurs.
|
||||||
"""
|
"""
|
||||||
if not isinstance(server, six.binary_type):
|
if not isinstance(server, bytes):
|
||||||
server = server.encode('utf-8')
|
server = server.encode('utf-8')
|
||||||
self._execute('erase', server)
|
self._execute('erase', server)
|
||||||
|
|
||||||
|
@ -72,36 +69,25 @@ class Store(object):
|
||||||
return json.loads(data.decode('utf-8'))
|
return json.loads(data.decode('utf-8'))
|
||||||
|
|
||||||
def _execute(self, subcmd, data_input):
|
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
|
output = None
|
||||||
env = create_environment_dict(self.environment)
|
env = create_environment_dict(self.environment)
|
||||||
try:
|
try:
|
||||||
if six.PY3:
|
output = subprocess.check_output(
|
||||||
output = subprocess.check_output(
|
[self.exe, subcmd], input=data_input, env=env,
|
||||||
[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:
|
except subprocess.CalledProcessError as e:
|
||||||
raise errors.process_store_error(e, self.program)
|
raise errors.process_store_error(e, self.program) from e
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
raise errors.StoreError(
|
raise errors.StoreError(
|
||||||
'{} not installed or not available in PATH'.format(
|
f'{self.program} not installed or not available in PATH'
|
||||||
self.program
|
) from e
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise errors.StoreError(
|
raise errors.StoreError(
|
||||||
'Unexpected OS error "{}", errno={}'.format(
|
f'Unexpected OS error "{e.strerror}", errno={e.errno}'
|
||||||
e.strerror, e.errno
|
) from e
|
||||||
)
|
|
||||||
)
|
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -1,32 +1,4 @@
|
||||||
import distutils.spawn
|
|
||||||
import os
|
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):
|
def create_environment_dict(overrides):
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
import requests
|
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):
|
class DockerException(Exception):
|
||||||
"""
|
"""
|
||||||
|
@ -18,17 +27,16 @@ def create_api_error_from_http_exception(e):
|
||||||
try:
|
try:
|
||||||
explanation = response.json()['message']
|
explanation = response.json()['message']
|
||||||
except ValueError:
|
except ValueError:
|
||||||
explanation = (response.content or '').strip()
|
explanation = (response.text or '').strip()
|
||||||
cls = APIError
|
cls = APIError
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
if explanation and ('No such image' in str(explanation) or
|
explanation_msg = (explanation or '').lower()
|
||||||
'not found: does not exist or no pull access'
|
if any(fragment in explanation_msg
|
||||||
in str(explanation) or
|
for fragment in _image_not_found_explanation_fragments):
|
||||||
'repository does not exist' in str(explanation)):
|
|
||||||
cls = ImageNotFound
|
cls = ImageNotFound
|
||||||
else:
|
else:
|
||||||
cls = NotFound
|
cls = NotFound
|
||||||
raise cls(e, response=response, explanation=explanation)
|
raise cls(e, response=response, explanation=explanation) from e
|
||||||
|
|
||||||
|
|
||||||
class APIError(requests.exceptions.HTTPError, DockerException):
|
class APIError(requests.exceptions.HTTPError, DockerException):
|
||||||
|
@ -38,25 +46,27 @@ class APIError(requests.exceptions.HTTPError, DockerException):
|
||||||
def __init__(self, message, response=None, explanation=None):
|
def __init__(self, message, response=None, explanation=None):
|
||||||
# requests 1.2 supports response as a keyword argument, but
|
# requests 1.2 supports response as a keyword argument, but
|
||||||
# requests 1.1 doesn't
|
# requests 1.1 doesn't
|
||||||
super(APIError, self).__init__(message)
|
super().__init__(message)
|
||||||
self.response = response
|
self.response = response
|
||||||
self.explanation = explanation
|
self.explanation = explanation
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
message = super(APIError, self).__str__()
|
message = super().__str__()
|
||||||
|
|
||||||
if self.is_client_error():
|
if self.is_client_error():
|
||||||
message = '{0} Client Error for {1}: {2}'.format(
|
message = (
|
||||||
self.response.status_code, self.response.url,
|
f'{self.response.status_code} Client Error for '
|
||||||
self.response.reason)
|
f'{self.response.url}: {self.response.reason}'
|
||||||
|
)
|
||||||
|
|
||||||
elif self.is_server_error():
|
elif self.is_server_error():
|
||||||
message = '{0} Server Error for {1}: {2}'.format(
|
message = (
|
||||||
self.response.status_code, self.response.url,
|
f'{self.response.status_code} Server Error for '
|
||||||
self.response.reason)
|
f'{self.response.url}: {self.response.reason}'
|
||||||
|
)
|
||||||
|
|
||||||
if self.explanation:
|
if self.explanation:
|
||||||
message = '{0} ("{1}")'.format(message, self.explanation)
|
message = f'{message} ("{self.explanation}")'
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
@ -133,11 +143,11 @@ class ContainerError(DockerException):
|
||||||
self.image = image
|
self.image = image
|
||||||
self.stderr = stderr
|
self.stderr = stderr
|
||||||
|
|
||||||
err = ": {}".format(stderr) if stderr is not None else ""
|
err = f": {stderr}" if stderr is not None else ""
|
||||||
msg = ("Command '{}' in image '{}' returned non-zero exit "
|
super().__init__(
|
||||||
"status {}{}").format(command, image, exit_status, err)
|
f"Command '{command}' in image '{image}' "
|
||||||
|
f"returned non-zero exit status {exit_status}{err}"
|
||||||
super(ContainerError, self).__init__(msg)
|
)
|
||||||
|
|
||||||
|
|
||||||
class StreamParseError(RuntimeError):
|
class StreamParseError(RuntimeError):
|
||||||
|
@ -147,7 +157,7 @@ class StreamParseError(RuntimeError):
|
||||||
|
|
||||||
class BuildError(DockerException):
|
class BuildError(DockerException):
|
||||||
def __init__(self, reason, build_log):
|
def __init__(self, reason, build_log):
|
||||||
super(BuildError, self).__init__(reason)
|
super().__init__(reason)
|
||||||
self.msg = reason
|
self.msg = reason
|
||||||
self.build_log = build_log
|
self.build_log = build_log
|
||||||
|
|
||||||
|
@ -157,8 +167,8 @@ class ImageLoadError(DockerException):
|
||||||
|
|
||||||
|
|
||||||
def create_unexpected_kwargs_error(name, kwargs):
|
def create_unexpected_kwargs_error(name, kwargs):
|
||||||
quoted_kwargs = ["'{}'".format(k) for k in sorted(kwargs)]
|
quoted_kwargs = [f"'{k}'" for k in sorted(kwargs)]
|
||||||
text = ["{}() ".format(name)]
|
text = [f"{name}() "]
|
||||||
if len(quoted_kwargs) == 1:
|
if len(quoted_kwargs) == 1:
|
||||||
text.append("got an unexpected keyword argument ")
|
text.append("got an unexpected keyword argument ")
|
||||||
else:
|
else:
|
||||||
|
@ -172,7 +182,7 @@ class MissingContextParameter(DockerException):
|
||||||
self.param = param
|
self.param = param
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return ("missing parameter: {}".format(self.param))
|
return (f"missing parameter: {self.param}")
|
||||||
|
|
||||||
|
|
||||||
class ContextAlreadyExists(DockerException):
|
class ContextAlreadyExists(DockerException):
|
||||||
|
@ -180,7 +190,7 @@ class ContextAlreadyExists(DockerException):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return ("context {} already exists".format(self.name))
|
return (f"context {self.name} already exists")
|
||||||
|
|
||||||
|
|
||||||
class ContextException(DockerException):
|
class ContextException(DockerException):
|
||||||
|
@ -196,4 +206,4 @@ class ContextNotFound(DockerException):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return ("context '{}' not found".format(self.name))
|
return (f"context '{self.name}' not found")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from .resource import Model, Collection
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Config(Model):
|
class Config(Model):
|
||||||
|
@ -7,7 +7,7 @@ class Config(Model):
|
||||||
id_attribute = 'ID'
|
id_attribute = 'ID'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
return f"<{self.__class__.__name__}: '{self.name}'>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -30,6 +30,7 @@ class ConfigCollection(Collection):
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
obj = self.client.api.create_config(**kwargs)
|
obj = self.client.api.create_config(**kwargs)
|
||||||
|
obj.setdefault("Spec", {})["Name"] = kwargs.get("name")
|
||||||
return self.prepare_model(obj)
|
return self.prepare_model(obj)
|
||||||
create.__doc__ = APIClient.create_config.__doc__
|
create.__doc__ = APIClient.create_config.__doc__
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,13 @@ from collections import namedtuple
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
||||||
from ..errors import (
|
from ..errors import (
|
||||||
ContainerError, DockerException, ImageNotFound,
|
ContainerError,
|
||||||
NotFound, create_unexpected_kwargs_error
|
DockerException,
|
||||||
|
ImageNotFound,
|
||||||
|
NotFound,
|
||||||
|
create_unexpected_kwargs_error,
|
||||||
)
|
)
|
||||||
from ..types import HostConfig
|
from ..types import HostConfig, NetworkingConfig
|
||||||
from ..utils import version_gte
|
from ..utils import version_gte
|
||||||
from .images import Image
|
from .images import Image
|
||||||
from .resource import Collection, Model
|
from .resource import Collection, Model
|
||||||
|
@ -21,6 +24,7 @@ class Container(Model):
|
||||||
query the Docker daemon for the current properties, causing
|
query the Docker daemon for the current properties, causing
|
||||||
:py:attr:`attrs` to be refreshed.
|
:py:attr:`attrs` to be refreshed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""
|
"""
|
||||||
|
@ -47,11 +51,11 @@ class Container(Model):
|
||||||
try:
|
try:
|
||||||
result = self.attrs['Config'].get('Labels')
|
result = self.attrs['Config'].get('Labels')
|
||||||
return result or {}
|
return result or {}
|
||||||
except KeyError:
|
except KeyError as ke:
|
||||||
raise DockerException(
|
raise DockerException(
|
||||||
'Label data is not available for sparse objects. Call reload()'
|
'Label data is not available for sparse objects. Call reload()'
|
||||||
' to retrieve all information'
|
' to retrieve all information'
|
||||||
)
|
) from ke
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
|
@ -62,6 +66,15 @@ class Container(Model):
|
||||||
return self.attrs['State']['Status']
|
return self.attrs['State']['Status']
|
||||||
return self.attrs['State']
|
return self.attrs['State']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def health(self):
|
||||||
|
"""
|
||||||
|
The healthcheck status of the container.
|
||||||
|
|
||||||
|
For example, ``healthy`, or ``unhealthy`.
|
||||||
|
"""
|
||||||
|
return self.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
"""
|
"""
|
||||||
|
@ -121,6 +134,7 @@ class Container(Model):
|
||||||
tag (str): The tag to push
|
tag (str): The tag to push
|
||||||
message (str): A commit message
|
message (str): A commit message
|
||||||
author (str): The name of the author
|
author (str): The name of the author
|
||||||
|
pause (bool): Whether to pause the container before committing
|
||||||
changes (str): Dockerfile instructions to apply while committing
|
changes (str): Dockerfile instructions to apply while committing
|
||||||
conf (dict): The configuration for the container. See the
|
conf (dict): The configuration for the container. See the
|
||||||
`Engine API documentation
|
`Engine API documentation
|
||||||
|
@ -141,7 +155,8 @@ class Container(Model):
|
||||||
Inspect changes on a container's filesystem.
|
Inspect changes on a container's filesystem.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(str)
|
(list) A list of dictionaries containing the attributes `Path`
|
||||||
|
and `Kind`.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -166,7 +181,8 @@ class Container(Model):
|
||||||
user (str): User to execute command as. Default: root
|
user (str): User to execute command as. Default: root
|
||||||
detach (bool): If true, detach from the exec command.
|
detach (bool): If true, detach from the exec command.
|
||||||
Default: False
|
Default: False
|
||||||
stream (bool): Stream response data. Default: False
|
stream (bool): Stream response data. Ignored if ``detach`` is true.
|
||||||
|
Default: False
|
||||||
socket (bool): Return the connection socket to allow custom
|
socket (bool): Return the connection socket to allow custom
|
||||||
read/write operations. Default: False
|
read/write operations. Default: False
|
||||||
environment (dict or list): A dictionary or a list of strings in
|
environment (dict or list): A dictionary or a list of strings in
|
||||||
|
@ -290,14 +306,15 @@ class Container(Model):
|
||||||
tail (str or int): Output specified number of lines at the end of
|
tail (str or int): Output specified number of lines at the end of
|
||||||
logs. Either an integer of number of lines or the string
|
logs. Either an integer of number of lines or the string
|
||||||
``all``. Default ``all``
|
``all``. Default ``all``
|
||||||
since (datetime or int): Show logs since a given datetime or
|
since (datetime, int, or float): Show logs since a given datetime,
|
||||||
integer epoch (in seconds)
|
integer epoch (in seconds) or float (in nanoseconds)
|
||||||
follow (bool): Follow log output. Default ``False``
|
follow (bool): Follow log output. Default ``False``
|
||||||
until (datetime or int): Show logs that occurred before the given
|
until (datetime, int, or float): Show logs that occurred before
|
||||||
datetime or integer epoch (in seconds)
|
the given datetime, integer epoch (in seconds), or
|
||||||
|
float (in nanoseconds)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(generator or str): Logs from the container.
|
(generator of bytes or bytes): Logs from the container.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -323,7 +340,7 @@ class Container(Model):
|
||||||
Args:
|
Args:
|
||||||
path (str): Path inside the container where the file(s) will be
|
path (str): Path inside the container where the file(s) will be
|
||||||
extracted. Must exist.
|
extracted. Must exist.
|
||||||
data (bytes): tar data to be extracted
|
data (bytes or stream): tar data to be extracted
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(bool): True if the call succeeds.
|
(bool): True if the call succeeds.
|
||||||
|
@ -553,6 +570,11 @@ class ContainerCollection(Collection):
|
||||||
``["SYS_ADMIN", "MKNOD"]``.
|
``["SYS_ADMIN", "MKNOD"]``.
|
||||||
cap_drop (list of str): Drop kernel capabilities.
|
cap_drop (list of str): Drop kernel capabilities.
|
||||||
cgroup_parent (str): Override the default parent cgroup.
|
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_count (int): Number of usable CPUs (Windows only).
|
||||||
cpu_percent (int): Usable percentage of the available CPUs
|
cpu_percent (int): Usable percentage of the available CPUs
|
||||||
(Windows only).
|
(Windows only).
|
||||||
|
@ -600,7 +622,28 @@ class ContainerCollection(Collection):
|
||||||
group_add (:py:class:`list`): List of additional group names and/or
|
group_add (:py:class:`list`): List of additional group names and/or
|
||||||
IDs that the container process will run as.
|
IDs that the container process will run as.
|
||||||
healthcheck (dict): Specify a test to perform to check that the
|
healthcheck (dict): Specify a test to perform to check that the
|
||||||
container is healthy.
|
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).
|
||||||
hostname (str): Optional hostname for the container.
|
hostname (str): Optional hostname for the container.
|
||||||
init (bool): Run an init inside the container that forwards
|
init (bool): Run an init inside the container that forwards
|
||||||
signals and reaps processes
|
signals and reaps processes
|
||||||
|
@ -644,7 +687,7 @@ class ContainerCollection(Collection):
|
||||||
network_mode (str): One of:
|
network_mode (str): One of:
|
||||||
|
|
||||||
- ``bridge`` Create a new network stack for the container on
|
- ``bridge`` Create a new network stack for the container on
|
||||||
on the bridge network.
|
the bridge network.
|
||||||
- ``none`` No networking for this container.
|
- ``none`` No networking for this container.
|
||||||
- ``container:<name|id>`` Reuse another container's network
|
- ``container:<name|id>`` Reuse another container's network
|
||||||
stack.
|
stack.
|
||||||
|
@ -652,6 +695,14 @@ class ContainerCollection(Collection):
|
||||||
This mode is incompatible with ``ports``.
|
This mode is incompatible with ``ports``.
|
||||||
|
|
||||||
Incompatible with ``network``.
|
Incompatible with ``network``.
|
||||||
|
networking_config (Dict[str, EndpointConfig]):
|
||||||
|
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_kill_disable (bool): Whether to disable OOM killer.
|
||||||
oom_score_adj (int): An integer value containing the score given
|
oom_score_adj (int): An integer value containing the score given
|
||||||
to the container in order to tune OOM killer preferences.
|
to the container in order to tune OOM killer preferences.
|
||||||
|
@ -761,6 +812,15 @@ class ContainerCollection(Collection):
|
||||||
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},
|
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},
|
||||||
'/var/www': {'bind': '/mnt/vol1', 'mode': 'ro'}}
|
'/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
|
volumes_from (:py:class:`list`): List of container names or IDs to
|
||||||
get volumes from.
|
get volumes from.
|
||||||
working_dir (str): Path to the working directory.
|
working_dir (str): Path to the working directory.
|
||||||
|
@ -792,7 +852,7 @@ class ContainerCollection(Collection):
|
||||||
image = image.id
|
image = image.id
|
||||||
stream = kwargs.pop('stream', False)
|
stream = kwargs.pop('stream', False)
|
||||||
detach = kwargs.pop('detach', False)
|
detach = kwargs.pop('detach', False)
|
||||||
platform = kwargs.pop('platform', None)
|
platform = kwargs.get('platform', None)
|
||||||
|
|
||||||
if detach and remove:
|
if detach and remove:
|
||||||
if version_gte(self.client.api._version, '1.25'):
|
if version_gte(self.client.api._version, '1.25'):
|
||||||
|
@ -807,6 +867,12 @@ class ContainerCollection(Collection):
|
||||||
'together.'
|
'together.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get('networking_config') and not kwargs.get('network'):
|
||||||
|
raise RuntimeError(
|
||||||
|
'The option "networking_config" can not be used '
|
||||||
|
'without "network".'
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container = self.create(image=image, command=command,
|
container = self.create(image=image, command=command,
|
||||||
detach=detach, **kwargs)
|
detach=detach, **kwargs)
|
||||||
|
@ -841,9 +907,9 @@ class ContainerCollection(Collection):
|
||||||
container, exit_status, command, image, out
|
container, exit_status, command, image, out
|
||||||
)
|
)
|
||||||
|
|
||||||
return out if stream or out is None else b''.join(
|
if stream or out is None:
|
||||||
[line for line in out]
|
return out
|
||||||
)
|
return b''.join(out)
|
||||||
|
|
||||||
def create(self, image, command=None, **kwargs):
|
def create(self, image, command=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -959,6 +1025,7 @@ class ContainerCollection(Collection):
|
||||||
|
|
||||||
def prune(self, filters=None):
|
def prune(self, filters=None):
|
||||||
return self.client.api.prune_containers(filters=filters)
|
return self.client.api.prune_containers(filters=filters)
|
||||||
|
|
||||||
prune.__doc__ = APIClient.prune_containers.__doc__
|
prune.__doc__ = APIClient.prune_containers.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
@ -976,6 +1043,7 @@ RUN_CREATE_KWARGS = [
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'name',
|
'name',
|
||||||
'network_disabled',
|
'network_disabled',
|
||||||
|
'platform',
|
||||||
'stdin_open',
|
'stdin_open',
|
||||||
'stop_signal',
|
'stop_signal',
|
||||||
'tty',
|
'tty',
|
||||||
|
@ -992,6 +1060,7 @@ RUN_HOST_CONFIG_KWARGS = [
|
||||||
'cap_add',
|
'cap_add',
|
||||||
'cap_drop',
|
'cap_drop',
|
||||||
'cgroup_parent',
|
'cgroup_parent',
|
||||||
|
'cgroupns',
|
||||||
'cpu_count',
|
'cpu_count',
|
||||||
'cpu_percent',
|
'cpu_percent',
|
||||||
'cpu_period',
|
'cpu_period',
|
||||||
|
@ -1075,8 +1144,17 @@ def _create_container_args(kwargs):
|
||||||
host_config_kwargs['binds'] = volumes
|
host_config_kwargs['binds'] = volumes
|
||||||
|
|
||||||
network = kwargs.pop('network', None)
|
network = kwargs.pop('network', None)
|
||||||
|
networking_config = kwargs.pop('networking_config', None)
|
||||||
if network:
|
if network:
|
||||||
create_kwargs['networking_config'] = {network: None}
|
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}
|
||||||
host_config_kwargs['network_mode'] = network
|
host_config_kwargs['network_mode'] = network
|
||||||
|
|
||||||
# All kwargs should have been consumed by this point, so raise
|
# All kwargs should have been consumed by this point, so raise
|
||||||
|
@ -1109,8 +1187,10 @@ def _host_volume_from_bind(bind):
|
||||||
bits = rest.split(':', 1)
|
bits = rest.split(':', 1)
|
||||||
if len(bits) == 1 or bits[1] in ('ro', 'rw'):
|
if len(bits) == 1 or bits[1] in ('ro', 'rw'):
|
||||||
return drive + bits[0]
|
return drive + bits[0]
|
||||||
|
elif bits[1].endswith(':ro') or bits[1].endswith(':rw'):
|
||||||
|
return bits[1][:-3]
|
||||||
else:
|
else:
|
||||||
return bits[1].rstrip(':ro').rstrip(':rw')
|
return bits[1]
|
||||||
|
|
||||||
|
|
||||||
ExecResult = namedtuple('ExecResult', 'exit_code,output')
|
ExecResult = namedtuple('ExecResult', 'exit_code,output')
|
||||||
|
|
|
@ -2,8 +2,6 @@ import itertools
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
from ..constants import DEFAULT_DATA_CHUNK_SIZE
|
||||||
from ..errors import BuildError, ImageLoadError, InvalidArgument
|
from ..errors import BuildError, ImageLoadError, InvalidArgument
|
||||||
|
@ -17,7 +15,8 @@ class Image(Model):
|
||||||
An image on the server.
|
An image on the server.
|
||||||
"""
|
"""
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (self.__class__.__name__, "', '".join(self.tags))
|
tag_str = "', '".join(self.tags)
|
||||||
|
return f"<{self.__class__.__name__}: '{tag_str}'>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def labels(self):
|
def labels(self):
|
||||||
|
@ -30,12 +29,12 @@ class Image(Model):
|
||||||
@property
|
@property
|
||||||
def short_id(self):
|
def short_id(self):
|
||||||
"""
|
"""
|
||||||
The ID of the image truncated to 10 characters, plus the ``sha256:``
|
The ID of the image truncated to 12 characters, plus the ``sha256:``
|
||||||
prefix.
|
prefix.
|
||||||
"""
|
"""
|
||||||
if self.id.startswith('sha256:'):
|
if self.id.startswith('sha256:'):
|
||||||
return self.id[:17]
|
return self.id[:19]
|
||||||
return self.id[:10]
|
return self.id[:12]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tags(self):
|
def tags(self):
|
||||||
|
@ -52,7 +51,7 @@ class Image(Model):
|
||||||
Show the history of an image.
|
Show the history of an image.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(str): The history of the image.
|
(list): The history of the image.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
@ -60,6 +59,24 @@ class Image(Model):
|
||||||
"""
|
"""
|
||||||
return self.client.api.history(self.id)
|
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):
|
def save(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE, named=False):
|
||||||
"""
|
"""
|
||||||
Get a tarball of an image. Similar to the ``docker save`` command.
|
Get a tarball of an image. Similar to the ``docker save`` command.
|
||||||
|
@ -84,19 +101,19 @@ class Image(Model):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> image = cli.get_image("busybox:latest")
|
>>> image = cli.images.get("busybox:latest")
|
||||||
>>> f = open('/tmp/busybox-latest.tar', 'wb')
|
>>> f = open('/tmp/busybox-latest.tar', 'wb')
|
||||||
>>> for chunk in image:
|
>>> for chunk in image.save():
|
||||||
>>> f.write(chunk)
|
>>> f.write(chunk)
|
||||||
>>> f.close()
|
>>> f.close()
|
||||||
"""
|
"""
|
||||||
img = self.id
|
img = self.id
|
||||||
if named:
|
if named:
|
||||||
img = self.tags[0] if self.tags else img
|
img = self.tags[0] if self.tags else img
|
||||||
if isinstance(named, six.string_types):
|
if isinstance(named, str):
|
||||||
if named not in self.tags:
|
if named not in self.tags:
|
||||||
raise InvalidArgument(
|
raise InvalidArgument(
|
||||||
"{} is not a valid tag for this image".format(named)
|
f"{named} is not a valid tag for this image"
|
||||||
)
|
)
|
||||||
img = named
|
img = named
|
||||||
|
|
||||||
|
@ -127,7 +144,7 @@ class RegistryData(Model):
|
||||||
Image metadata stored on the registry, including available platforms.
|
Image metadata stored on the registry, including available platforms.
|
||||||
"""
|
"""
|
||||||
def __init__(self, image_name, *args, **kwargs):
|
def __init__(self, image_name, *args, **kwargs):
|
||||||
super(RegistryData, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.image_name = image_name
|
self.image_name = image_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -140,10 +157,10 @@ class RegistryData(Model):
|
||||||
@property
|
@property
|
||||||
def short_id(self):
|
def short_id(self):
|
||||||
"""
|
"""
|
||||||
The ID of the image truncated to 10 characters, plus the ``sha256:``
|
The ID of the image truncated to 12 characters, plus the ``sha256:``
|
||||||
prefix.
|
prefix.
|
||||||
"""
|
"""
|
||||||
return self.id[:17]
|
return self.id[:19]
|
||||||
|
|
||||||
def pull(self, platform=None):
|
def pull(self, platform=None):
|
||||||
"""
|
"""
|
||||||
|
@ -180,7 +197,7 @@ class RegistryData(Model):
|
||||||
parts = platform.split('/')
|
parts = platform.split('/')
|
||||||
if len(parts) > 3 or len(parts) < 1:
|
if len(parts) > 3 or len(parts) < 1:
|
||||||
raise InvalidArgument(
|
raise InvalidArgument(
|
||||||
'"{0}" is not a valid platform descriptor'.format(platform)
|
f'"{platform}" is not a valid platform descriptor'
|
||||||
)
|
)
|
||||||
platform = {'os': parts[0]}
|
platform = {'os': parts[0]}
|
||||||
if len(parts) > 2:
|
if len(parts) > 2:
|
||||||
|
@ -205,10 +222,10 @@ class ImageCollection(Collection):
|
||||||
Build an image and return it. Similar to the ``docker build``
|
Build an image and return it. Similar to the ``docker build``
|
||||||
command. Either ``path`` or ``fileobj`` must be set.
|
command. Either ``path`` or ``fileobj`` must be set.
|
||||||
|
|
||||||
If you have a tar file for the Docker build context (including a
|
If you already have a tar file for the Docker build context (including
|
||||||
Dockerfile) already, pass a readable file-like object to ``fileobj``
|
a Dockerfile), pass a readable file-like object to ``fileobj``
|
||||||
and also pass ``custom_context=True``. If the stream is compressed
|
and also pass ``custom_context=True``. If the stream is also
|
||||||
also, set ``encoding`` to the correct value (e.g ``gzip``).
|
compressed, set ``encoding`` to the correct value (e.g ``gzip``).
|
||||||
|
|
||||||
If you want to get the raw output of the build, use the
|
If you want to get the raw output of the build, use the
|
||||||
:py:meth:`~docker.api.build.BuildApiMixin.build` method in the
|
:py:meth:`~docker.api.build.BuildApiMixin.build` method in the
|
||||||
|
@ -265,7 +282,7 @@ class ImageCollection(Collection):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(tuple): The first item is the :py:class:`Image` object for the
|
(tuple): The first item is the :py:class:`Image` object for the
|
||||||
image that was build. The second item is a generator of the
|
image that was built. The second item is a generator of the
|
||||||
build logs as JSON-decoded objects.
|
build logs as JSON-decoded objects.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
@ -277,7 +294,7 @@ class ImageCollection(Collection):
|
||||||
If neither ``path`` nor ``fileobj`` is specified.
|
If neither ``path`` nor ``fileobj`` is specified.
|
||||||
"""
|
"""
|
||||||
resp = self.client.api.build(**kwargs)
|
resp = self.client.api.build(**kwargs)
|
||||||
if isinstance(resp, six.string_types):
|
if isinstance(resp, str):
|
||||||
return self.get(resp)
|
return self.get(resp)
|
||||||
last_event = None
|
last_event = None
|
||||||
image_id = None
|
image_id = None
|
||||||
|
@ -390,8 +407,8 @@ class ImageCollection(Collection):
|
||||||
if match:
|
if match:
|
||||||
image_id = match.group(2)
|
image_id = match.group(2)
|
||||||
images.append(image_id)
|
images.append(image_id)
|
||||||
if 'error' in chunk:
|
if 'errorDetail' in chunk:
|
||||||
raise ImageLoadError(chunk['error'])
|
raise ImageLoadError(chunk['errorDetail']['message'])
|
||||||
|
|
||||||
return [self.get(i) for i in images]
|
return [self.get(i) for i in images]
|
||||||
|
|
||||||
|
@ -439,7 +456,8 @@ class ImageCollection(Collection):
|
||||||
if 'stream' in kwargs:
|
if 'stream' in kwargs:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'`stream` is not a valid parameter for this method'
|
'`stream` is not a valid parameter for this method'
|
||||||
' and will be overridden'
|
' and will be overridden',
|
||||||
|
stacklevel=1,
|
||||||
)
|
)
|
||||||
del kwargs['stream']
|
del kwargs['stream']
|
||||||
|
|
||||||
|
@ -452,9 +470,8 @@ class ImageCollection(Collection):
|
||||||
# to be pulled.
|
# to be pulled.
|
||||||
pass
|
pass
|
||||||
if not all_tags:
|
if not all_tags:
|
||||||
return self.get('{0}{2}{1}'.format(
|
sep = '@' if tag.startswith('sha256:') else ':'
|
||||||
repository, tag, '@' if tag.startswith('sha256:') else ':'
|
return self.get(f'{repository}{sep}{tag}')
|
||||||
))
|
|
||||||
return self.list(repository)
|
return self.list(repository)
|
||||||
|
|
||||||
def push(self, repository, tag=None, **kwargs):
|
def push(self, repository, tag=None, **kwargs):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from ..utils import version_gte
|
from ..utils import version_gte
|
||||||
from .containers import Container
|
from .containers import Container
|
||||||
from .resource import Model, Collection
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Network(Model):
|
class Network(Model):
|
||||||
|
@ -184,7 +184,7 @@ class NetworkCollection(Collection):
|
||||||
|
|
||||||
def list(self, *args, **kwargs):
|
def list(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
List networks. Similar to the ``docker networks ls`` command.
|
List networks. Similar to the ``docker network ls`` command.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
names (:py:class:`list`): List of names to filter by.
|
names (:py:class:`list`): List of names to filter by.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .resource import Model, Collection
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Node(Model):
|
class Node(Model):
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Plugin(Model):
|
||||||
A plugin on the server.
|
A plugin on the server.
|
||||||
"""
|
"""
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
return f"<{self.__class__.__name__}: '{self.name}'>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -44,16 +44,19 @@ class Plugin(Model):
|
||||||
self.client.api.configure_plugin(self.name, options)
|
self.client.api.configure_plugin(self.name, options)
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def disable(self):
|
def disable(self, force=False):
|
||||||
"""
|
"""
|
||||||
Disable the plugin.
|
Disable the plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force (bool): Force disable. Default: False
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
If the server returns an error.
|
If the server returns an error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.client.api.disable_plugin(self.name)
|
self.client.api.disable_plugin(self.name, force)
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def enable(self, timeout=0):
|
def enable(self, timeout=0):
|
||||||
|
@ -117,8 +120,11 @@ class Plugin(Model):
|
||||||
if remote is None:
|
if remote is None:
|
||||||
remote = self.name
|
remote = self.name
|
||||||
privileges = self.client.api.plugin_privileges(remote)
|
privileges = self.client.api.plugin_privileges(remote)
|
||||||
for d in self.client.api.upgrade_plugin(self.name, remote, privileges):
|
yield from self.client.api.upgrade_plugin(
|
||||||
yield d
|
self.name,
|
||||||
|
remote,
|
||||||
|
privileges,
|
||||||
|
)
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,7 +187,7 @@ class PluginCollection(Collection):
|
||||||
"""
|
"""
|
||||||
privileges = self.client.api.plugin_privileges(remote_name)
|
privileges = self.client.api.plugin_privileges(remote_name)
|
||||||
it = self.client.api.pull_plugin(remote_name, privileges, local_name)
|
it = self.client.api.pull_plugin(remote_name, privileges, local_name)
|
||||||
for data in it:
|
for _data in it:
|
||||||
pass
|
pass
|
||||||
return self.get(local_name or remote_name)
|
return self.get(local_name or remote_name)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
class Model:
|
||||||
class Model(object):
|
|
||||||
"""
|
"""
|
||||||
A base class for representing a single object on the server.
|
A base class for representing a single object on the server.
|
||||||
"""
|
"""
|
||||||
|
@ -18,13 +17,13 @@ class Model(object):
|
||||||
self.attrs = {}
|
self.attrs = {}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: %s>" % (self.__class__.__name__, self.short_id)
|
return f"<{self.__class__.__name__}: {self.short_id}>"
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, self.__class__) and self.id == other.id
|
return isinstance(other, self.__class__) and self.id == other.id
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash("%s:%s" % (self.__class__.__name__, self.id))
|
return hash(f"{self.__class__.__name__}:{self.id}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
|
@ -36,9 +35,9 @@ class Model(object):
|
||||||
@property
|
@property
|
||||||
def short_id(self):
|
def short_id(self):
|
||||||
"""
|
"""
|
||||||
The ID of the object, truncated to 10 characters.
|
The ID of the object, truncated to 12 characters.
|
||||||
"""
|
"""
|
||||||
return self.id[:10]
|
return self.id[:12]
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""
|
"""
|
||||||
|
@ -49,7 +48,7 @@ class Model(object):
|
||||||
self.attrs = new_model.attrs
|
self.attrs = new_model.attrs
|
||||||
|
|
||||||
|
|
||||||
class Collection(object):
|
class Collection:
|
||||||
"""
|
"""
|
||||||
A base class for representing all objects of a particular type on the
|
A base class for representing all objects of a particular type on the
|
||||||
server.
|
server.
|
||||||
|
@ -65,9 +64,10 @@ class Collection(object):
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"'{}' object is not callable. You might be trying to use the old "
|
f"'{self.__class__.__name__}' object is not callable. "
|
||||||
"(pre-2.0) API - use docker.APIClient if so."
|
"You might be trying to use the old (pre-2.0) API - "
|
||||||
.format(self.__class__.__name__))
|
"use docker.APIClient if so."
|
||||||
|
)
|
||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -89,5 +89,4 @@ class Collection(object):
|
||||||
elif isinstance(attrs, dict):
|
elif isinstance(attrs, dict):
|
||||||
return self.model(attrs=attrs, client=self.client, collection=self)
|
return self.model(attrs=attrs, client=self.client, collection=self)
|
||||||
else:
|
else:
|
||||||
raise Exception("Can't create %s from %s" %
|
raise Exception(f"Can't create {self.model.__name__} from {attrs}")
|
||||||
(self.model.__name__, attrs))
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from .resource import Model, Collection
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Secret(Model):
|
class Secret(Model):
|
||||||
|
@ -7,7 +7,7 @@ class Secret(Model):
|
||||||
id_attribute = 'ID'
|
id_attribute = 'ID'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
return f"<{self.__class__.__name__}: '{self.name}'>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import copy
|
import copy
|
||||||
from docker.errors import create_unexpected_kwargs_error, InvalidArgument
|
|
||||||
from docker.types import TaskTemplate, ContainerSpec, Placement, ServiceMode
|
from docker.errors import InvalidArgument, create_unexpected_kwargs_error
|
||||||
from .resource import Model, Collection
|
from docker.types import ContainerSpec, Placement, ServiceMode, TaskTemplate
|
||||||
|
|
||||||
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Service(Model):
|
class Service(Model):
|
||||||
|
@ -213,6 +215,12 @@ class ServiceCollection(Collection):
|
||||||
to the service.
|
to the service.
|
||||||
privileges (Privileges): Security options for the service's
|
privileges (Privileges): Security options for the service's
|
||||||
containers.
|
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:
|
Returns:
|
||||||
:py:class:`Service`: The created service.
|
:py:class:`Service`: The created service.
|
||||||
|
@ -260,6 +268,8 @@ class ServiceCollection(Collection):
|
||||||
filters (dict): Filters to process on the nodes list. Valid
|
filters (dict): Filters to process on the nodes list. Valid
|
||||||
filters: ``id``, ``name`` , ``label`` and ``mode``.
|
filters: ``id``, ``name`` , ``label`` and ``mode``.
|
||||||
Default: ``None``.
|
Default: ``None``.
|
||||||
|
status (bool): Include the service task count of running and
|
||||||
|
desired tasks. Default: ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of :py:class:`Service`: The services.
|
list of :py:class:`Service`: The services.
|
||||||
|
@ -277,6 +287,8 @@ class ServiceCollection(Collection):
|
||||||
# kwargs to copy straight over to ContainerSpec
|
# kwargs to copy straight over to ContainerSpec
|
||||||
CONTAINER_SPEC_KWARGS = [
|
CONTAINER_SPEC_KWARGS = [
|
||||||
'args',
|
'args',
|
||||||
|
'cap_add',
|
||||||
|
'cap_drop',
|
||||||
'command',
|
'command',
|
||||||
'configs',
|
'configs',
|
||||||
'dns_config',
|
'dns_config',
|
||||||
|
@ -299,6 +311,7 @@ CONTAINER_SPEC_KWARGS = [
|
||||||
'tty',
|
'tty',
|
||||||
'user',
|
'user',
|
||||||
'workdir',
|
'workdir',
|
||||||
|
'sysctls',
|
||||||
]
|
]
|
||||||
|
|
||||||
# kwargs to copy straight over to TaskTemplate
|
# kwargs to copy straight over to TaskTemplate
|
||||||
|
@ -314,6 +327,7 @@ CREATE_SERVICE_KWARGS = [
|
||||||
'labels',
|
'labels',
|
||||||
'mode',
|
'mode',
|
||||||
'update_config',
|
'update_config',
|
||||||
|
'rollback_config',
|
||||||
'endpoint_spec',
|
'endpoint_spec',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from docker.api import APIClient
|
from docker.api import APIClient
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
|
|
||||||
from .resource import Model
|
from .resource import Model
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ class Swarm(Model):
|
||||||
id_attribute = 'ID'
|
id_attribute = 'ID'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Swarm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.client:
|
if self.client:
|
||||||
try:
|
try:
|
||||||
self.reload()
|
self.reload()
|
||||||
|
@ -35,7 +36,8 @@ class Swarm(Model):
|
||||||
|
|
||||||
def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
|
def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
|
||||||
force_new_cluster=False, default_addr_pool=None,
|
force_new_cluster=False, default_addr_pool=None,
|
||||||
subnet_size=None, data_path_addr=None, **kwargs):
|
subnet_size=None, data_path_addr=None, data_path_port=None,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize a new swarm on this Engine.
|
Initialize a new swarm on this Engine.
|
||||||
|
|
||||||
|
@ -65,6 +67,9 @@ class Swarm(Model):
|
||||||
networks created from the default subnet pool. Default: None
|
networks created from the default subnet pool. Default: None
|
||||||
data_path_addr (string): Address or interface to use for data path
|
data_path_addr (string): Address or interface to use for data path
|
||||||
traffic. For example, 192.168.1.1, or an interface, like eth0.
|
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
|
task_history_retention_limit (int): Maximum number of tasks
|
||||||
history stored.
|
history stored.
|
||||||
snapshot_interval (int): Number of logs entries between snapshot.
|
snapshot_interval (int): Number of logs entries between snapshot.
|
||||||
|
@ -121,6 +126,7 @@ class Swarm(Model):
|
||||||
'default_addr_pool': default_addr_pool,
|
'default_addr_pool': default_addr_pool,
|
||||||
'subnet_size': subnet_size,
|
'subnet_size': subnet_size,
|
||||||
'data_path_addr': data_path_addr,
|
'data_path_addr': data_path_addr,
|
||||||
|
'data_path_port': data_path_port,
|
||||||
}
|
}
|
||||||
init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs)
|
init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs)
|
||||||
node_id = self.client.api.init_swarm(**init_kwargs)
|
node_id = self.client.api.init_swarm(**init_kwargs)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from ..api import APIClient
|
from ..api import APIClient
|
||||||
from .resource import Model, Collection
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
class Volume(Model):
|
class Volume(Model):
|
||||||
|
|
|
@ -1,67 +1,30 @@
|
||||||
import os
|
import os
|
||||||
import ssl
|
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from .transport import SSLHTTPAdapter
|
|
||||||
|
|
||||||
|
|
||||||
class TLSConfig(object):
|
class TLSConfig:
|
||||||
"""
|
"""
|
||||||
TLS configuration.
|
TLS configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client_cert (tuple of str): Path to client cert, path to client key.
|
client_cert (tuple of str): Path to client cert, path to client key.
|
||||||
ca_cert (str): Path to CA cert file.
|
ca_cert (str): Path to CA cert file.
|
||||||
verify (bool or str): This can be ``False`` or a path to a CA cert
|
verify (bool or str): This can be a bool or a path to a CA cert
|
||||||
file.
|
file to verify against. If ``True``, verify using ca_cert;
|
||||||
ssl_version (int): A valid `SSL version`_.
|
if ``False`` or not specified, do not verify.
|
||||||
assert_hostname (bool): Verify the hostname of the server.
|
|
||||||
|
|
||||||
.. _`SSL version`:
|
|
||||||
https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1
|
|
||||||
"""
|
"""
|
||||||
cert = None
|
cert = None
|
||||||
ca_cert = None
|
ca_cert = None
|
||||||
verify = None
|
verify = None
|
||||||
ssl_version = None
|
|
||||||
|
|
||||||
def __init__(self, client_cert=None, ca_cert=None, verify=None,
|
def __init__(self, client_cert=None, ca_cert=None, verify=None):
|
||||||
ssl_version=None, assert_hostname=None,
|
|
||||||
assert_fingerprint=None):
|
|
||||||
# Argument compatibility/mapping with
|
# Argument compatibility/mapping with
|
||||||
# https://docs.docker.com/engine/articles/https/
|
# https://docs.docker.com/engine/articles/https/
|
||||||
# This diverges from the Docker CLI in that users can specify 'tls'
|
# This diverges from the Docker CLI in that users can specify 'tls'
|
||||||
# here, but also disable any public/default CA pool verification by
|
# here, but also disable any public/default CA pool verification by
|
||||||
# leaving verify=False
|
# leaving verify=False
|
||||||
|
|
||||||
self.assert_hostname = assert_hostname
|
|
||||||
self.assert_fingerprint = assert_fingerprint
|
|
||||||
|
|
||||||
# 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
|
# "client_cert" must have both or neither cert/key files. In
|
||||||
# either case, Alert the user when both are expected, but any are
|
# either case, Alert the user when both are expected, but any are
|
||||||
# missing.
|
# missing.
|
||||||
|
@ -73,7 +36,7 @@ class TLSConfig(object):
|
||||||
raise errors.TLSParameterError(
|
raise errors.TLSParameterError(
|
||||||
'client_cert must be a tuple of'
|
'client_cert must be a tuple of'
|
||||||
' (client certificate, key file)'
|
' (client certificate, key file)'
|
||||||
)
|
) from None
|
||||||
|
|
||||||
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
|
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
|
||||||
not os.path.isfile(tls_key)):
|
not os.path.isfile(tls_key)):
|
||||||
|
@ -95,8 +58,6 @@ class TLSConfig(object):
|
||||||
"""
|
"""
|
||||||
Configure a client with these TLS options.
|
Configure a client with these TLS options.
|
||||||
"""
|
"""
|
||||||
client.ssl_version = self.ssl_version
|
|
||||||
|
|
||||||
if self.verify and self.ca_cert:
|
if self.verify and self.ca_cert:
|
||||||
client.verify = self.ca_cert
|
client.verify = self.ca_cert
|
||||||
else:
|
else:
|
||||||
|
@ -104,9 +65,3 @@ class TLSConfig(object):
|
||||||
|
|
||||||
if self.cert:
|
if self.cert:
|
||||||
client.cert = self.cert
|
client.cert = self.cert
|
||||||
|
|
||||||
client.mount('https://', SSLHTTPAdapter(
|
|
||||||
ssl_version=self.ssl_version,
|
|
||||||
assert_hostname=self.assert_hostname,
|
|
||||||
assert_fingerprint=self.assert_fingerprint,
|
|
||||||
))
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# flake8: noqa
|
|
||||||
from .unixconn import UnixHTTPAdapter
|
from .unixconn import UnixHTTPAdapter
|
||||||
from .ssladapter import SSLHTTPAdapter
|
|
||||||
try:
|
try:
|
||||||
from .npipeconn import NpipeHTTPAdapter
|
from .npipeconn import NpipeHTTPAdapter
|
||||||
from .npipesocket import NpipeSocket
|
from .npipesocket import NpipeSocket
|
||||||
|
|
|
@ -3,6 +3,11 @@ import requests.adapters
|
||||||
|
|
||||||
class BaseHTTPAdapter(requests.adapters.HTTPAdapter):
|
class BaseHTTPAdapter(requests.adapters.HTTPAdapter):
|
||||||
def close(self):
|
def close(self):
|
||||||
super(BaseHTTPAdapter, self).close()
|
super().close()
|
||||||
if hasattr(self, 'pools'):
|
if hasattr(self, 'pools'):
|
||||||
self.pools.clear()
|
self.pools.clear()
|
||||||
|
|
||||||
|
# Fix for requests 2.32.2+:
|
||||||
|
# https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05
|
||||||
|
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
|
||||||
|
return self.get_connection(request.url, proxies)
|
||||||
|
|
|
@ -1,26 +1,19 @@
|
||||||
import six
|
import queue
|
||||||
|
|
||||||
import requests.adapters
|
import requests.adapters
|
||||||
|
import urllib3
|
||||||
|
import urllib3.connection
|
||||||
|
|
||||||
from docker.transport.basehttpadapter import BaseHTTPAdapter
|
|
||||||
from .. import constants
|
from .. import constants
|
||||||
|
from .basehttpadapter import BaseHTTPAdapter
|
||||||
from .npipesocket import NpipeSocket
|
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
|
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||||
|
|
||||||
|
|
||||||
class NpipeHTTPConnection(httplib.HTTPConnection, object):
|
class NpipeHTTPConnection(urllib3.connection.HTTPConnection):
|
||||||
def __init__(self, npipe_path, timeout=60):
|
def __init__(self, npipe_path, timeout=60):
|
||||||
super(NpipeHTTPConnection, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout
|
'localhost', timeout=timeout
|
||||||
)
|
)
|
||||||
self.npipe_path = npipe_path
|
self.npipe_path = npipe_path
|
||||||
|
@ -35,7 +28,7 @@ class NpipeHTTPConnection(httplib.HTTPConnection, object):
|
||||||
|
|
||||||
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||||
def __init__(self, npipe_path, timeout=60, maxsize=10):
|
def __init__(self, npipe_path, timeout=60, maxsize=10):
|
||||||
super(NpipeHTTPConnectionPool, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout, maxsize=maxsize
|
'localhost', timeout=timeout, maxsize=maxsize
|
||||||
)
|
)
|
||||||
self.npipe_path = npipe_path
|
self.npipe_path = npipe_path
|
||||||
|
@ -53,18 +46,17 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = self.pool.get(block=self.block, timeout=timeout)
|
conn = self.pool.get(block=self.block, timeout=timeout)
|
||||||
|
except AttributeError as ae: # self.pool is None
|
||||||
|
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae
|
||||||
|
|
||||||
except AttributeError: # self.pool is None
|
except queue.Empty:
|
||||||
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
|
|
||||||
|
|
||||||
except six.moves.queue.Empty:
|
|
||||||
if self.block:
|
if self.block:
|
||||||
raise urllib3.exceptions.EmptyPoolError(
|
raise urllib3.exceptions.EmptyPoolError(
|
||||||
self,
|
self,
|
||||||
"Pool reached maximum size and no more "
|
"Pool reached maximum size and no more "
|
||||||
"connections are allowed."
|
"connections are allowed."
|
||||||
)
|
) from None
|
||||||
pass # Oh well, we'll create a new connection then
|
# Oh well, we'll create a new connection then
|
||||||
|
|
||||||
return conn or self._new_conn()
|
return conn or self._new_conn()
|
||||||
|
|
||||||
|
@ -85,7 +77,7 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
|
||||||
self.pools = RecentlyUsedContainer(
|
self.pools = RecentlyUsedContainer(
|
||||||
pool_connections, dispose_func=lambda p: p.close()
|
pool_connections, dispose_func=lambda p: p.close()
|
||||||
)
|
)
|
||||||
super(NpipeHTTPAdapter, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
def get_connection(self, url, proxies=None):
|
def get_connection(self, url, proxies=None):
|
||||||
with self.pools.lock:
|
with self.pools.lock:
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import functools
|
import functools
|
||||||
import time
|
|
||||||
import io
|
import io
|
||||||
|
import time
|
||||||
|
|
||||||
import six
|
import pywintypes
|
||||||
|
import win32api
|
||||||
|
import win32event
|
||||||
import win32file
|
import win32file
|
||||||
import win32pipe
|
import win32pipe
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ def check_closed(f):
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
class NpipeSocket(object):
|
class NpipeSocket:
|
||||||
""" Partial implementation of the socket API over windows named pipes.
|
""" Partial implementation of the socket API over windows named pipes.
|
||||||
This implementation is only designed to be used as a client socket,
|
This implementation is only designed to be used as a client socket,
|
||||||
and server-specific methods (bind, listen, accept...) are not
|
and server-specific methods (bind, listen, accept...) are not
|
||||||
|
@ -55,7 +57,9 @@ class NpipeSocket(object):
|
||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
win32file.OPEN_EXISTING,
|
win32file.OPEN_EXISTING,
|
||||||
cSECURITY_ANONYMOUS | cSECURITY_SQOS_PRESENT,
|
(cSECURITY_ANONYMOUS
|
||||||
|
| cSECURITY_SQOS_PRESENT
|
||||||
|
| win32file.FILE_FLAG_OVERLAPPED),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
except win32pipe.error as e:
|
except win32pipe.error as e:
|
||||||
|
@ -128,29 +132,41 @@ class NpipeSocket(object):
|
||||||
|
|
||||||
@check_closed
|
@check_closed
|
||||||
def recv_into(self, buf, nbytes=0):
|
def recv_into(self, buf, nbytes=0):
|
||||||
if six.PY2:
|
|
||||||
return self._recv_into_py2(buf, nbytes)
|
|
||||||
|
|
||||||
readbuf = buf
|
readbuf = buf
|
||||||
if not isinstance(buf, memoryview):
|
if not isinstance(buf, memoryview):
|
||||||
readbuf = memoryview(buf)
|
readbuf = memoryview(buf)
|
||||||
|
|
||||||
err, data = win32file.ReadFile(
|
event = win32event.CreateEvent(None, True, True, None)
|
||||||
self._handle,
|
try:
|
||||||
readbuf[:nbytes] if nbytes else readbuf
|
overlapped = pywintypes.OVERLAPPED()
|
||||||
)
|
overlapped.hEvent = event
|
||||||
return len(data)
|
err, data = win32file.ReadFile(
|
||||||
|
self._handle,
|
||||||
def _recv_into_py2(self, buf, nbytes):
|
readbuf[:nbytes] if nbytes else readbuf,
|
||||||
err, data = win32file.ReadFile(self._handle, nbytes or len(buf))
|
overlapped
|
||||||
n = len(data)
|
)
|
||||||
buf[:n] = data
|
wait_result = win32event.WaitForSingleObject(event, self._timeout)
|
||||||
return n
|
if wait_result == win32event.WAIT_TIMEOUT:
|
||||||
|
win32file.CancelIo(self._handle)
|
||||||
|
raise TimeoutError
|
||||||
|
return win32file.GetOverlappedResult(self._handle, overlapped, 0)
|
||||||
|
finally:
|
||||||
|
win32api.CloseHandle(event)
|
||||||
|
|
||||||
@check_closed
|
@check_closed
|
||||||
def send(self, string, flags=0):
|
def send(self, string, flags=0):
|
||||||
err, nbytes = win32file.WriteFile(self._handle, string)
|
event = win32event.CreateEvent(None, True, True, None)
|
||||||
return nbytes
|
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)
|
||||||
|
|
||||||
@check_closed
|
@check_closed
|
||||||
def sendall(self, string, flags=0):
|
def sendall(self, string, flags=0):
|
||||||
|
@ -169,15 +185,12 @@ class NpipeSocket(object):
|
||||||
def settimeout(self, value):
|
def settimeout(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
# Blocking mode
|
# Blocking mode
|
||||||
self._timeout = win32pipe.NMPWAIT_WAIT_FOREVER
|
self._timeout = win32event.INFINITE
|
||||||
elif not isinstance(value, (float, int)) or value < 0:
|
elif not isinstance(value, (float, int)) or value < 0:
|
||||||
raise ValueError('Timeout value out of range')
|
raise ValueError('Timeout value out of range')
|
||||||
elif value == 0:
|
|
||||||
# Non-blocking mode
|
|
||||||
self._timeout = win32pipe.NMPWAIT_NO_WAIT
|
|
||||||
else:
|
else:
|
||||||
# Timeout mode - Value converted to milliseconds
|
# Timeout mode - Value converted to milliseconds
|
||||||
self._timeout = value * 1000
|
self._timeout = int(value * 1000)
|
||||||
|
|
||||||
def gettimeout(self):
|
def gettimeout(self):
|
||||||
return self._timeout
|
return self._timeout
|
||||||
|
@ -195,7 +208,7 @@ class NpipeFileIOBase(io.RawIOBase):
|
||||||
self.sock = npipe_socket
|
self.sock = npipe_socket
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
super(NpipeFileIOBase, self).close()
|
super().close()
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
|
||||||
def fileno(self):
|
def fileno(self):
|
||||||
|
|
|
@ -1,31 +1,25 @@
|
||||||
import paramiko
|
|
||||||
import requests.adapters
|
|
||||||
import six
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import queue
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
import requests.adapters
|
||||||
|
import urllib3
|
||||||
|
import urllib3.connection
|
||||||
|
|
||||||
from docker.transport.basehttpadapter import BaseHTTPAdapter
|
|
||||||
from .. import constants
|
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
|
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||||
|
|
||||||
|
|
||||||
class SSHSocket(socket.socket):
|
class SSHSocket(socket.socket):
|
||||||
def __init__(self, host):
|
def __init__(self, host):
|
||||||
super(SSHSocket, self).__init__(
|
super().__init__(
|
||||||
socket.AF_INET, socket.SOCK_STREAM)
|
socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = None
|
self.port = None
|
||||||
|
@ -60,12 +54,11 @@ class SSHSocket(socket.socket):
|
||||||
env.pop('SSL_CERT_FILE', None)
|
env.pop('SSL_CERT_FILE', None)
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
' '.join(args),
|
args,
|
||||||
env=env,
|
env=env,
|
||||||
shell=True,
|
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
preexec_fn=None if constants.IS_WINDOWS_PLATFORM else preexec_func)
|
preexec_fn=preexec_func)
|
||||||
|
|
||||||
def _write(self, data):
|
def _write(self, data):
|
||||||
if not self.proc or self.proc.stdin.closed:
|
if not self.proc or self.proc.stdin.closed:
|
||||||
|
@ -90,8 +83,7 @@ class SSHSocket(socket.socket):
|
||||||
def makefile(self, mode):
|
def makefile(self, mode):
|
||||||
if not self.proc:
|
if not self.proc:
|
||||||
self.connect()
|
self.connect()
|
||||||
if six.PY3:
|
self.proc.stdout.channel = self
|
||||||
self.proc.stdout.channel = self
|
|
||||||
|
|
||||||
return self.proc.stdout
|
return self.proc.stdout
|
||||||
|
|
||||||
|
@ -103,9 +95,9 @@ class SSHSocket(socket.socket):
|
||||||
self.proc.terminate()
|
self.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
class SSHConnection(httplib.HTTPConnection, object):
|
class SSHConnection(urllib3.connection.HTTPConnection):
|
||||||
def __init__(self, ssh_transport=None, timeout=60, host=None):
|
def __init__(self, ssh_transport=None, timeout=60, host=None):
|
||||||
super(SSHConnection, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout
|
'localhost', timeout=timeout
|
||||||
)
|
)
|
||||||
self.ssh_transport = ssh_transport
|
self.ssh_transport = ssh_transport
|
||||||
|
@ -129,7 +121,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||||
scheme = 'ssh'
|
scheme = 'ssh'
|
||||||
|
|
||||||
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
|
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
|
||||||
super(SSHConnectionPool, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout, maxsize=maxsize
|
'localhost', timeout=timeout, maxsize=maxsize
|
||||||
)
|
)
|
||||||
self.ssh_transport = None
|
self.ssh_transport = None
|
||||||
|
@ -149,17 +141,17 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||||
try:
|
try:
|
||||||
conn = self.pool.get(block=self.block, timeout=timeout)
|
conn = self.pool.get(block=self.block, timeout=timeout)
|
||||||
|
|
||||||
except AttributeError: # self.pool is None
|
except AttributeError as ae: # self.pool is None
|
||||||
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
|
raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae
|
||||||
|
|
||||||
except six.moves.queue.Empty:
|
except queue.Empty:
|
||||||
if self.block:
|
if self.block:
|
||||||
raise urllib3.exceptions.EmptyPoolError(
|
raise urllib3.exceptions.EmptyPoolError(
|
||||||
self,
|
self,
|
||||||
"Pool reached maximum size and no more "
|
"Pool reached maximum size and no more "
|
||||||
"connections are allowed."
|
"connections are allowed."
|
||||||
)
|
) from None
|
||||||
pass # Oh well, we'll create a new connection then
|
# Oh well, we'll create a new connection then
|
||||||
|
|
||||||
return conn or self._new_conn()
|
return conn or self._new_conn()
|
||||||
|
|
||||||
|
@ -188,12 +180,12 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||||
self.pools = RecentlyUsedContainer(
|
self.pools = RecentlyUsedContainer(
|
||||||
pool_connections, dispose_func=lambda p: p.close()
|
pool_connections, dispose_func=lambda p: p.close()
|
||||||
)
|
)
|
||||||
super(SSHHTTPAdapter, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
def _create_paramiko_client(self, base_url):
|
def _create_paramiko_client(self, base_url):
|
||||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||||
self.ssh_client = paramiko.SSHClient()
|
self.ssh_client = paramiko.SSHClient()
|
||||||
base_url = six.moves.urllib_parse.urlparse(base_url)
|
base_url = urllib.parse.urlparse(base_url)
|
||||||
self.ssh_params = {
|
self.ssh_params = {
|
||||||
"hostname": base_url.hostname,
|
"hostname": base_url.hostname,
|
||||||
"port": base_url.port,
|
"port": base_url.port,
|
||||||
|
@ -205,20 +197,21 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||||
with open(ssh_config_file) as f:
|
with open(ssh_config_file) as f:
|
||||||
conf.parse(f)
|
conf.parse(f)
|
||||||
host_config = conf.lookup(base_url.hostname)
|
host_config = conf.lookup(base_url.hostname)
|
||||||
self.ssh_conf = host_config
|
|
||||||
if 'proxycommand' in host_config:
|
if 'proxycommand' in host_config:
|
||||||
self.ssh_params["sock"] = paramiko.ProxyCommand(
|
self.ssh_params["sock"] = paramiko.ProxyCommand(
|
||||||
self.ssh_conf['proxycommand']
|
host_config['proxycommand']
|
||||||
)
|
)
|
||||||
if 'hostname' in host_config:
|
if 'hostname' in host_config:
|
||||||
self.ssh_params['hostname'] = host_config['hostname']
|
self.ssh_params['hostname'] = host_config['hostname']
|
||||||
if base_url.port is None and 'port' in host_config:
|
if base_url.port is None and 'port' in host_config:
|
||||||
self.ssh_params['port'] = self.ssh_conf['port']
|
self.ssh_params['port'] = host_config['port']
|
||||||
if base_url.username is None and 'user' in host_config:
|
if base_url.username is None and 'user' in host_config:
|
||||||
self.ssh_params['username'] = self.ssh_conf['user']
|
self.ssh_params['username'] = host_config['user']
|
||||||
|
if 'identityfile' in host_config:
|
||||||
|
self.ssh_params['key_filename'] = host_config['identityfile']
|
||||||
|
|
||||||
self.ssh_client.load_system_host_keys()
|
self.ssh_client.load_system_host_keys()
|
||||||
self.ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy())
|
self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
if self.ssh_client:
|
if self.ssh_client:
|
||||||
|
@ -252,6 +245,6 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
super(SSHHTTPAdapter, self).close()
|
super().close()
|
||||||
if self.ssh_client:
|
if self.ssh_client:
|
||||||
self.ssh_client.close()
|
self.ssh_client.close()
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
""" 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')
|
|
|
@ -1,41 +1,24 @@
|
||||||
import six
|
|
||||||
import requests.adapters
|
|
||||||
import socket
|
import socket
|
||||||
from six.moves import http_client as httplib
|
|
||||||
|
|
||||||
from docker.transport.basehttpadapter import BaseHTTPAdapter
|
import requests.adapters
|
||||||
|
import urllib3
|
||||||
|
import urllib3.connection
|
||||||
|
|
||||||
from .. import constants
|
from .. import constants
|
||||||
|
from .basehttpadapter import BaseHTTPAdapter
|
||||||
try:
|
|
||||||
import requests.packages.urllib3 as urllib3
|
|
||||||
except ImportError:
|
|
||||||
import urllib3
|
|
||||||
|
|
||||||
|
|
||||||
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||||
|
|
||||||
|
|
||||||
class UnixHTTPResponse(httplib.HTTPResponse, object):
|
class UnixHTTPConnection(urllib3.connection.HTTPConnection):
|
||||||
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):
|
def __init__(self, base_url, unix_socket, timeout=60):
|
||||||
super(UnixHTTPConnection, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout
|
'localhost', timeout=timeout
|
||||||
)
|
)
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.unix_socket = unix_socket
|
self.unix_socket = unix_socket
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.disable_buffering = False
|
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
@ -43,21 +26,10 @@ class UnixHTTPConnection(httplib.HTTPConnection, object):
|
||||||
sock.connect(self.unix_socket)
|
sock.connect(self.unix_socket)
|
||||||
self.sock = sock
|
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):
|
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||||
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
|
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
|
||||||
super(UnixHTTPConnectionPool, self).__init__(
|
super().__init__(
|
||||||
'localhost', timeout=timeout, maxsize=maxsize
|
'localhost', timeout=timeout, maxsize=maxsize
|
||||||
)
|
)
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
@ -82,14 +54,14 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
|
||||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
|
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
|
||||||
socket_path = socket_url.replace('http+unix://', '')
|
socket_path = socket_url.replace('http+unix://', '')
|
||||||
if not socket_path.startswith('/'):
|
if not socket_path.startswith('/'):
|
||||||
socket_path = '/' + socket_path
|
socket_path = f"/{socket_path}"
|
||||||
self.socket_path = socket_path
|
self.socket_path = socket_path
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.max_pool_size = max_pool_size
|
self.max_pool_size = max_pool_size
|
||||||
self.pools = RecentlyUsedContainer(
|
self.pools = RecentlyUsedContainer(
|
||||||
pool_connections, dispose_func=lambda p: p.close()
|
pool_connections, dispose_func=lambda p: p.close()
|
||||||
)
|
)
|
||||||
super(UnixHTTPAdapter, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
def get_connection(self, url, proxies=None):
|
def get_connection(self, url, proxies=None):
|
||||||
with self.pools.lock:
|
with self.pools.lock:
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
# flake8: noqa
|
from .containers import ContainerConfig, DeviceRequest, HostConfig, LogConfig, Ulimit
|
||||||
from .containers import (
|
|
||||||
ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest
|
|
||||||
)
|
|
||||||
from .daemon import CancellableStream
|
from .daemon import CancellableStream
|
||||||
from .healthcheck import Healthcheck
|
from .healthcheck import Healthcheck
|
||||||
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
|
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
|
||||||
from .services import (
|
from .services import (
|
||||||
ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec,
|
ConfigReference,
|
||||||
Mount, Placement, PlacementPreference, Privileges, Resources,
|
ContainerSpec,
|
||||||
RestartPolicy, RollbackConfig, SecretReference, ServiceMode, TaskTemplate,
|
DNSConfig,
|
||||||
UpdateConfig, NetworkAttachmentConfig
|
DriverConfig,
|
||||||
|
EndpointSpec,
|
||||||
|
Mount,
|
||||||
|
NetworkAttachmentConfig,
|
||||||
|
Placement,
|
||||||
|
PlacementPreference,
|
||||||
|
Privileges,
|
||||||
|
Resources,
|
||||||
|
RestartPolicy,
|
||||||
|
RollbackConfig,
|
||||||
|
SecretReference,
|
||||||
|
ServiceMode,
|
||||||
|
TaskTemplate,
|
||||||
|
UpdateConfig,
|
||||||
)
|
)
|
||||||
from .swarm import SwarmSpec, SwarmExternalCA
|
from .swarm import SwarmExternalCA, SwarmSpec
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class DictType(dict):
|
class DictType(dict):
|
||||||
def __init__(self, init):
|
def __init__(self, init):
|
||||||
for k, v in six.iteritems(init):
|
for k, v in init.items():
|
||||||
self[k] = v
|
self[k] = v
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
import six
|
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from ..utils.utils import (
|
from ..utils.utils import (
|
||||||
convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds,
|
convert_port_bindings,
|
||||||
format_environment, format_extra_hosts, normalize_links, parse_bytes,
|
convert_tmpfs_mounts,
|
||||||
parse_devices, split_command, version_gte, version_lt,
|
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 .base import DictType
|
||||||
from .healthcheck import Healthcheck
|
from .healthcheck import Healthcheck
|
||||||
|
|
||||||
|
|
||||||
class LogConfigTypesEnum(object):
|
class LogConfigTypesEnum:
|
||||||
_values = (
|
_values = (
|
||||||
'json-file',
|
'json-file',
|
||||||
'syslog',
|
'syslog',
|
||||||
|
@ -50,8 +56,11 @@ class LogConfig(DictType):
|
||||||
>>> container = client.create_container('busybox', 'true',
|
>>> container = client.create_container('busybox', 'true',
|
||||||
... host_config=hc)
|
... host_config=hc)
|
||||||
>>> client.inspect_container(container)['HostConfig']['LogConfig']
|
>>> client.inspect_container(container)['HostConfig']['LogConfig']
|
||||||
{'Type': 'json-file', 'Config': {'labels': 'production_status,geo', 'max-size': '1g'}}
|
{
|
||||||
""" # noqa: E501
|
'Type': 'json-file',
|
||||||
|
'Config': {'labels': 'production_status,geo', 'max-size': '1g'}
|
||||||
|
}
|
||||||
|
"""
|
||||||
types = LogConfigTypesEnum
|
types = LogConfigTypesEnum
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -61,7 +70,7 @@ class LogConfig(DictType):
|
||||||
if config and not isinstance(config, dict):
|
if config and not isinstance(config, dict):
|
||||||
raise ValueError("LogConfig.config must be a dictionary")
|
raise ValueError("LogConfig.config must be a dictionary")
|
||||||
|
|
||||||
super(LogConfig, self).__init__({
|
super().__init__({
|
||||||
'Type': log_driver_type,
|
'Type': log_driver_type,
|
||||||
'Config': config
|
'Config': config
|
||||||
})
|
})
|
||||||
|
@ -117,13 +126,13 @@ class Ulimit(DictType):
|
||||||
name = kwargs.get('name', kwargs.get('Name'))
|
name = kwargs.get('name', kwargs.get('Name'))
|
||||||
soft = kwargs.get('soft', kwargs.get('Soft'))
|
soft = kwargs.get('soft', kwargs.get('Soft'))
|
||||||
hard = kwargs.get('hard', kwargs.get('Hard'))
|
hard = kwargs.get('hard', kwargs.get('Hard'))
|
||||||
if not isinstance(name, six.string_types):
|
if not isinstance(name, str):
|
||||||
raise ValueError("Ulimit.name must be a string")
|
raise ValueError("Ulimit.name must be a string")
|
||||||
if soft and not isinstance(soft, int):
|
if soft and not isinstance(soft, int):
|
||||||
raise ValueError("Ulimit.soft must be an integer")
|
raise ValueError("Ulimit.soft must be an integer")
|
||||||
if hard and not isinstance(hard, int):
|
if hard and not isinstance(hard, int):
|
||||||
raise ValueError("Ulimit.hard must be an integer")
|
raise ValueError("Ulimit.hard must be an integer")
|
||||||
super(Ulimit, self).__init__({
|
super().__init__({
|
||||||
'Name': name,
|
'Name': name,
|
||||||
'Soft': soft,
|
'Soft': soft,
|
||||||
'Hard': hard
|
'Hard': hard
|
||||||
|
@ -184,7 +193,7 @@ class DeviceRequest(DictType):
|
||||||
|
|
||||||
if driver is None:
|
if driver is None:
|
||||||
driver = ''
|
driver = ''
|
||||||
elif not isinstance(driver, six.string_types):
|
elif not isinstance(driver, str):
|
||||||
raise ValueError('DeviceRequest.driver must be a string')
|
raise ValueError('DeviceRequest.driver must be a string')
|
||||||
if count is None:
|
if count is None:
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -203,7 +212,7 @@ class DeviceRequest(DictType):
|
||||||
elif not isinstance(options, dict):
|
elif not isinstance(options, dict):
|
||||||
raise ValueError('DeviceRequest.options must be a dict')
|
raise ValueError('DeviceRequest.options must be a dict')
|
||||||
|
|
||||||
super(DeviceRequest, self).__init__({
|
super().__init__({
|
||||||
'Driver': driver,
|
'Driver': driver,
|
||||||
'Count': count,
|
'Count': count,
|
||||||
'DeviceIDs': device_ids,
|
'DeviceIDs': device_ids,
|
||||||
|
@ -274,7 +283,8 @@ class HostConfig(dict):
|
||||||
volume_driver=None, cpu_count=None, cpu_percent=None,
|
volume_driver=None, cpu_count=None, cpu_percent=None,
|
||||||
nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None,
|
nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None,
|
||||||
cpu_rt_period=None, cpu_rt_runtime=None,
|
cpu_rt_period=None, cpu_rt_runtime=None,
|
||||||
device_cgroup_rules=None, device_requests=None):
|
device_cgroup_rules=None, device_requests=None,
|
||||||
|
cgroupns=None):
|
||||||
|
|
||||||
if mem_limit is not None:
|
if mem_limit is not None:
|
||||||
self['Memory'] = parse_bytes(mem_limit)
|
self['Memory'] = parse_bytes(mem_limit)
|
||||||
|
@ -297,7 +307,7 @@ class HostConfig(dict):
|
||||||
self['MemorySwappiness'] = mem_swappiness
|
self['MemorySwappiness'] = mem_swappiness
|
||||||
|
|
||||||
if shm_size is not None:
|
if shm_size is not None:
|
||||||
if isinstance(shm_size, six.string_types):
|
if isinstance(shm_size, str):
|
||||||
shm_size = parse_bytes(shm_size)
|
shm_size = parse_bytes(shm_size)
|
||||||
|
|
||||||
self['ShmSize'] = shm_size
|
self['ShmSize'] = shm_size
|
||||||
|
@ -358,7 +368,7 @@ class HostConfig(dict):
|
||||||
self['Devices'] = parse_devices(devices)
|
self['Devices'] = parse_devices(devices)
|
||||||
|
|
||||||
if group_add:
|
if group_add:
|
||||||
self['GroupAdd'] = [six.text_type(grp) for grp in group_add]
|
self['GroupAdd'] = [str(grp) for grp in group_add]
|
||||||
|
|
||||||
if dns is not None:
|
if dns is not None:
|
||||||
self['Dns'] = dns
|
self['Dns'] = dns
|
||||||
|
@ -378,11 +388,11 @@ class HostConfig(dict):
|
||||||
if not isinstance(sysctls, dict):
|
if not isinstance(sysctls, dict):
|
||||||
raise host_config_type_error('sysctls', sysctls, 'dict')
|
raise host_config_type_error('sysctls', sysctls, 'dict')
|
||||||
self['Sysctls'] = {}
|
self['Sysctls'] = {}
|
||||||
for k, v in six.iteritems(sysctls):
|
for k, v in sysctls.items():
|
||||||
self['Sysctls'][k] = six.text_type(v)
|
self['Sysctls'][k] = str(v)
|
||||||
|
|
||||||
if volumes_from is not None:
|
if volumes_from is not None:
|
||||||
if isinstance(volumes_from, six.string_types):
|
if isinstance(volumes_from, str):
|
||||||
volumes_from = volumes_from.split(',')
|
volumes_from = volumes_from.split(',')
|
||||||
|
|
||||||
self['VolumesFrom'] = volumes_from
|
self['VolumesFrom'] = volumes_from
|
||||||
|
@ -404,7 +414,7 @@ class HostConfig(dict):
|
||||||
|
|
||||||
if isinstance(lxc_conf, dict):
|
if isinstance(lxc_conf, dict):
|
||||||
formatted = []
|
formatted = []
|
||||||
for k, v in six.iteritems(lxc_conf):
|
for k, v in lxc_conf.items():
|
||||||
formatted.append({'Key': k, 'Value': str(v)})
|
formatted.append({'Key': k, 'Value': str(v)})
|
||||||
lxc_conf = formatted
|
lxc_conf = formatted
|
||||||
|
|
||||||
|
@ -559,7 +569,7 @@ class HostConfig(dict):
|
||||||
self["PidsLimit"] = pids_limit
|
self["PidsLimit"] = pids_limit
|
||||||
|
|
||||||
if isolation:
|
if isolation:
|
||||||
if not isinstance(isolation, six.string_types):
|
if not isinstance(isolation, str):
|
||||||
raise host_config_type_error('isolation', isolation, 'string')
|
raise host_config_type_error('isolation', isolation, 'string')
|
||||||
if version_lt(version, '1.24'):
|
if version_lt(version, '1.24'):
|
||||||
raise host_config_version_error('isolation', '1.24')
|
raise host_config_version_error('isolation', '1.24')
|
||||||
|
@ -609,7 +619,7 @@ class HostConfig(dict):
|
||||||
self['CpuPercent'] = cpu_percent
|
self['CpuPercent'] = cpu_percent
|
||||||
|
|
||||||
if nano_cpus:
|
if nano_cpus:
|
||||||
if not isinstance(nano_cpus, six.integer_types):
|
if not isinstance(nano_cpus, int):
|
||||||
raise host_config_type_error('nano_cpus', nano_cpus, 'int')
|
raise host_config_type_error('nano_cpus', nano_cpus, 'int')
|
||||||
if version_lt(version, '1.25'):
|
if version_lt(version, '1.25'):
|
||||||
raise host_config_version_error('nano_cpus', '1.25')
|
raise host_config_version_error('nano_cpus', '1.25')
|
||||||
|
@ -648,27 +658,30 @@ class HostConfig(dict):
|
||||||
req = DeviceRequest(**req)
|
req = DeviceRequest(**req)
|
||||||
self['DeviceRequests'].append(req)
|
self['DeviceRequests'].append(req)
|
||||||
|
|
||||||
|
if cgroupns:
|
||||||
|
self['CgroupnsMode'] = cgroupns
|
||||||
|
|
||||||
|
|
||||||
def host_config_type_error(param, param_value, expected):
|
def host_config_type_error(param, param_value, expected):
|
||||||
error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
|
return TypeError(
|
||||||
return TypeError(error_msg.format(param, expected, type(param_value)))
|
f'Invalid type for {param} param: expected {expected} '
|
||||||
|
f'but found {type(param_value)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def host_config_version_error(param, version, less_than=True):
|
def host_config_version_error(param, version, less_than=True):
|
||||||
operator = '<' if less_than else '>'
|
operator = '<' if less_than else '>'
|
||||||
error_msg = '{0} param is not supported in API versions {1} {2}'
|
return errors.InvalidVersion(
|
||||||
return errors.InvalidVersion(error_msg.format(param, operator, version))
|
f'{param} param is not supported in API versions {operator} {version}',
|
||||||
|
)
|
||||||
|
|
||||||
def host_config_value_error(param, param_value):
|
def host_config_value_error(param, param_value):
|
||||||
error_msg = 'Invalid value for {0} param: {1}'
|
return ValueError(f'Invalid value for {param} param: {param_value}')
|
||||||
return ValueError(error_msg.format(param, param_value))
|
|
||||||
|
|
||||||
|
|
||||||
def host_config_incompatible_error(param, param_value, incompatible_param):
|
def host_config_incompatible_error(param, param_value, incompatible_param):
|
||||||
error_msg = '\"{1}\" {0} is incompatible with {2}'
|
|
||||||
return errors.InvalidArgument(
|
return errors.InvalidArgument(
|
||||||
error_msg.format(param, param_value, incompatible_param)
|
f'\"{param_value}\" {param} is incompatible with {incompatible_param}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -699,17 +712,17 @@ class ContainerConfig(dict):
|
||||||
'version 1.29'
|
'version 1.29'
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(command, six.string_types):
|
if isinstance(command, str):
|
||||||
command = split_command(command)
|
command = split_command(command)
|
||||||
|
|
||||||
if isinstance(entrypoint, six.string_types):
|
if isinstance(entrypoint, str):
|
||||||
entrypoint = split_command(entrypoint)
|
entrypoint = split_command(entrypoint)
|
||||||
|
|
||||||
if isinstance(environment, dict):
|
if isinstance(environment, dict):
|
||||||
environment = format_environment(environment)
|
environment = format_environment(environment)
|
||||||
|
|
||||||
if isinstance(labels, list):
|
if isinstance(labels, list):
|
||||||
labels = dict((lbl, six.text_type('')) for lbl in labels)
|
labels = {lbl: '' for lbl in labels}
|
||||||
|
|
||||||
if isinstance(ports, list):
|
if isinstance(ports, list):
|
||||||
exposed_ports = {}
|
exposed_ports = {}
|
||||||
|
@ -720,10 +733,10 @@ class ContainerConfig(dict):
|
||||||
if len(port_definition) == 2:
|
if len(port_definition) == 2:
|
||||||
proto = port_definition[1]
|
proto = port_definition[1]
|
||||||
port = port_definition[0]
|
port = port_definition[0]
|
||||||
exposed_ports['{0}/{1}'.format(port, proto)] = {}
|
exposed_ports[f'{port}/{proto}'] = {}
|
||||||
ports = exposed_ports
|
ports = exposed_ports
|
||||||
|
|
||||||
if isinstance(volumes, six.string_types):
|
if isinstance(volumes, str):
|
||||||
volumes = [volumes, ]
|
volumes = [volumes, ]
|
||||||
|
|
||||||
if isinstance(volumes, list):
|
if isinstance(volumes, list):
|
||||||
|
@ -752,7 +765,7 @@ class ContainerConfig(dict):
|
||||||
'Hostname': hostname,
|
'Hostname': hostname,
|
||||||
'Domainname': domainname,
|
'Domainname': domainname,
|
||||||
'ExposedPorts': ports,
|
'ExposedPorts': ports,
|
||||||
'User': six.text_type(user) if user is not None else None,
|
'User': str(user) if user is not None else None,
|
||||||
'Tty': tty,
|
'Tty': tty,
|
||||||
'OpenStdin': stdin_open,
|
'OpenStdin': stdin_open,
|
||||||
'StdinOnce': stdin_once,
|
'StdinOnce': stdin_once,
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
try:
|
import urllib3
|
||||||
import requests.packages.urllib3 as urllib3
|
|
||||||
except ImportError:
|
|
||||||
import urllib3
|
|
||||||
|
|
||||||
from ..errors import DockerException
|
from ..errors import DockerException
|
||||||
|
|
||||||
|
|
||||||
class CancellableStream(object):
|
class CancellableStream:
|
||||||
"""
|
"""
|
||||||
Stream wrapper for real-time events, logs, etc. from the server.
|
Stream wrapper for real-time events, logs, etc. from the server.
|
||||||
|
|
||||||
|
@ -31,9 +28,9 @@ class CancellableStream(object):
|
||||||
try:
|
try:
|
||||||
return next(self._stream)
|
return next(self._stream)
|
||||||
except urllib3.exceptions.ProtocolError:
|
except urllib3.exceptions.ProtocolError:
|
||||||
raise StopIteration
|
raise StopIteration from None
|
||||||
except socket.error:
|
except OSError:
|
||||||
raise StopIteration
|
raise StopIteration from None
|
||||||
|
|
||||||
next = __next__
|
next = __next__
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
from .base import DictType
|
from .base import DictType
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class Healthcheck(DictType):
|
class Healthcheck(DictType):
|
||||||
"""
|
"""
|
||||||
|
@ -31,7 +29,7 @@ class Healthcheck(DictType):
|
||||||
"""
|
"""
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
test = kwargs.get('test', kwargs.get('Test'))
|
test = kwargs.get('test', kwargs.get('Test'))
|
||||||
if isinstance(test, six.string_types):
|
if isinstance(test, str):
|
||||||
test = ["CMD-SHELL", test]
|
test = ["CMD-SHELL", test]
|
||||||
|
|
||||||
interval = kwargs.get('interval', kwargs.get('Interval'))
|
interval = kwargs.get('interval', kwargs.get('Interval'))
|
||||||
|
@ -39,7 +37,7 @@ class Healthcheck(DictType):
|
||||||
retries = kwargs.get('retries', kwargs.get('Retries'))
|
retries = kwargs.get('retries', kwargs.get('Retries'))
|
||||||
start_period = kwargs.get('start_period', kwargs.get('StartPeriod'))
|
start_period = kwargs.get('start_period', kwargs.get('StartPeriod'))
|
||||||
|
|
||||||
super(Healthcheck, self).__init__({
|
super().__init__({
|
||||||
'Test': test,
|
'Test': test,
|
||||||
'Interval': interval,
|
'Interval': interval,
|
||||||
'Timeout': timeout,
|
'Timeout': timeout,
|
||||||
|
@ -53,7 +51,7 @@ class Healthcheck(DictType):
|
||||||
|
|
||||||
@test.setter
|
@test.setter
|
||||||
def test(self, value):
|
def test(self, value):
|
||||||
if isinstance(value, six.string_types):
|
if isinstance(value, str):
|
||||||
value = ["CMD-SHELL", value]
|
value = ["CMD-SHELL", value]
|
||||||
self['Test'] = value
|
self['Test'] = value
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ from ..utils import normalize_links, version_lt
|
||||||
|
|
||||||
class EndpointConfig(dict):
|
class EndpointConfig(dict):
|
||||||
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
|
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
|
||||||
ipv6_address=None, link_local_ips=None, driver_opt=None):
|
ipv6_address=None, link_local_ips=None, driver_opt=None,
|
||||||
|
mac_address=None):
|
||||||
if version_lt(version, '1.22'):
|
if version_lt(version, '1.22'):
|
||||||
raise errors.InvalidVersion(
|
raise errors.InvalidVersion(
|
||||||
'Endpoint config is not supported for API version < 1.22'
|
'Endpoint config is not supported for API version < 1.22'
|
||||||
|
@ -23,6 +24,13 @@ class EndpointConfig(dict):
|
||||||
if ipv6_address:
|
if ipv6_address:
|
||||||
ipam_config['IPv6Address'] = 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 link_local_ips is not None:
|
||||||
if version_lt(version, '1.24'):
|
if version_lt(version, '1.24'):
|
||||||
raise errors.InvalidVersion(
|
raise errors.InvalidVersion(
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import six
|
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from ..constants import IS_WINDOWS_PLATFORM
|
from ..constants import IS_WINDOWS_PLATFORM
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
check_resource, format_environment, format_extra_hosts, parse_bytes,
|
check_resource,
|
||||||
split_command, convert_service_networks,
|
convert_service_networks,
|
||||||
|
format_environment,
|
||||||
|
format_extra_hosts,
|
||||||
|
parse_bytes,
|
||||||
|
split_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +33,7 @@ class TaskTemplate(dict):
|
||||||
force_update (int): A counter that triggers an update even if no
|
force_update (int): A counter that triggers an update even if no
|
||||||
relevant parameters have been changed.
|
relevant parameters have been changed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, container_spec, resources=None, restart_policy=None,
|
def __init__(self, container_spec, resources=None, restart_policy=None,
|
||||||
placement=None, log_driver=None, networks=None,
|
placement=None, log_driver=None, networks=None,
|
||||||
force_update=None):
|
force_update=None):
|
||||||
|
@ -112,16 +115,24 @@ class ContainerSpec(dict):
|
||||||
containers. Only used for Windows containers.
|
containers. Only used for Windows containers.
|
||||||
init (boolean): Run an init inside the container that forwards signals
|
init (boolean): Run an init inside the container that forwards signals
|
||||||
and reaps processes.
|
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,
|
def __init__(self, image, command=None, args=None, hostname=None, env=None,
|
||||||
workdir=None, user=None, labels=None, mounts=None,
|
workdir=None, user=None, labels=None, mounts=None,
|
||||||
stop_grace_period=None, secrets=None, tty=None, groups=None,
|
stop_grace_period=None, secrets=None, tty=None, groups=None,
|
||||||
open_stdin=None, read_only=None, stop_signal=None,
|
open_stdin=None, read_only=None, stop_signal=None,
|
||||||
healthcheck=None, hosts=None, dns_config=None, configs=None,
|
healthcheck=None, hosts=None, dns_config=None, configs=None,
|
||||||
privileges=None, isolation=None, init=None):
|
privileges=None, isolation=None, init=None, cap_add=None,
|
||||||
|
cap_drop=None, sysctls=None):
|
||||||
self['Image'] = image
|
self['Image'] = image
|
||||||
|
|
||||||
if isinstance(command, six.string_types):
|
if isinstance(command, str):
|
||||||
command = split_command(command)
|
command = split_command(command)
|
||||||
self['Command'] = command
|
self['Command'] = command
|
||||||
self['Args'] = args
|
self['Args'] = args
|
||||||
|
@ -151,7 +162,7 @@ class ContainerSpec(dict):
|
||||||
if mounts is not None:
|
if mounts is not None:
|
||||||
parsed_mounts = []
|
parsed_mounts = []
|
||||||
for mount in mounts:
|
for mount in mounts:
|
||||||
if isinstance(mount, six.string_types):
|
if isinstance(mount, str):
|
||||||
parsed_mounts.append(Mount.parse_mount_string(mount))
|
parsed_mounts.append(Mount.parse_mount_string(mount))
|
||||||
else:
|
else:
|
||||||
# If mount already parsed
|
# If mount already parsed
|
||||||
|
@ -188,6 +199,24 @@ class ContainerSpec(dict):
|
||||||
if init is not None:
|
if init is not None:
|
||||||
self['Init'] = init
|
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):
|
class Mount(dict):
|
||||||
"""
|
"""
|
||||||
|
@ -213,18 +242,20 @@ class Mount(dict):
|
||||||
for the ``volume`` type.
|
for the ``volume`` type.
|
||||||
driver_config (DriverConfig): Volume driver configuration. Only valid
|
driver_config (DriverConfig): Volume driver configuration. Only valid
|
||||||
for the ``volume`` type.
|
for the ``volume`` type.
|
||||||
|
subpath (str): Path inside a volume to mount instead of the volume root.
|
||||||
tmpfs_size (int or string): The size for the tmpfs mount in bytes.
|
tmpfs_size (int or string): The size for the tmpfs mount in bytes.
|
||||||
tmpfs_mode (int): The permission mode for the tmpfs mount.
|
tmpfs_mode (int): The permission mode for the tmpfs mount.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, target, source, type='volume', read_only=False,
|
def __init__(self, target, source, type='volume', read_only=False,
|
||||||
consistency=None, propagation=None, no_copy=False,
|
consistency=None, propagation=None, no_copy=False,
|
||||||
labels=None, driver_config=None, tmpfs_size=None,
|
labels=None, driver_config=None, tmpfs_size=None,
|
||||||
tmpfs_mode=None):
|
tmpfs_mode=None, subpath=None):
|
||||||
self['Target'] = target
|
self['Target'] = target
|
||||||
self['Source'] = source
|
self['Source'] = source
|
||||||
if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
|
if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'Unsupported mount type: "{}"'.format(type)
|
f'Unsupported mount type: "{type}"'
|
||||||
)
|
)
|
||||||
self['Type'] = type
|
self['Type'] = type
|
||||||
self['ReadOnly'] = read_only
|
self['ReadOnly'] = read_only
|
||||||
|
@ -237,7 +268,7 @@ class Mount(dict):
|
||||||
self['BindOptions'] = {
|
self['BindOptions'] = {
|
||||||
'Propagation': propagation
|
'Propagation': propagation
|
||||||
}
|
}
|
||||||
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]):
|
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode, subpath]):
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'Incompatible options have been provided for the bind '
|
'Incompatible options have been provided for the bind '
|
||||||
'type mount.'
|
'type mount.'
|
||||||
|
@ -250,6 +281,8 @@ class Mount(dict):
|
||||||
volume_opts['Labels'] = labels
|
volume_opts['Labels'] = labels
|
||||||
if driver_config:
|
if driver_config:
|
||||||
volume_opts['DriverConfig'] = driver_config
|
volume_opts['DriverConfig'] = driver_config
|
||||||
|
if subpath:
|
||||||
|
volume_opts['Subpath'] = subpath
|
||||||
if volume_opts:
|
if volume_opts:
|
||||||
self['VolumeOptions'] = volume_opts
|
self['VolumeOptions'] = volume_opts
|
||||||
if any([propagation, tmpfs_size, tmpfs_mode]):
|
if any([propagation, tmpfs_size, tmpfs_mode]):
|
||||||
|
@ -260,7 +293,7 @@ class Mount(dict):
|
||||||
elif type == 'tmpfs':
|
elif type == 'tmpfs':
|
||||||
tmpfs_opts = {}
|
tmpfs_opts = {}
|
||||||
if tmpfs_mode:
|
if tmpfs_mode:
|
||||||
if not isinstance(tmpfs_mode, six.integer_types):
|
if not isinstance(tmpfs_mode, int):
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'tmpfs_mode must be an integer'
|
'tmpfs_mode must be an integer'
|
||||||
)
|
)
|
||||||
|
@ -280,7 +313,7 @@ class Mount(dict):
|
||||||
parts = string.split(':')
|
parts = string.split(':')
|
||||||
if len(parts) > 3:
|
if len(parts) > 3:
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'Invalid mount format "{0}"'.format(string)
|
f'Invalid mount format "{string}"'
|
||||||
)
|
)
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return cls(target=parts[0], source=None)
|
return cls(target=parts[0], source=None)
|
||||||
|
@ -316,6 +349,7 @@ class Resources(dict):
|
||||||
``{ resource_name: resource_value }``. Alternatively, a list of
|
``{ resource_name: resource_value }``. Alternatively, a list of
|
||||||
of resource specifications as defined by the Engine API.
|
of resource specifications as defined by the Engine API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
|
def __init__(self, cpu_limit=None, mem_limit=None, cpu_reservation=None,
|
||||||
mem_reservation=None, generic_resources=None):
|
mem_reservation=None, generic_resources=None):
|
||||||
limits = {}
|
limits = {}
|
||||||
|
@ -343,20 +377,20 @@ def _convert_generic_resources_dict(generic_resources):
|
||||||
return generic_resources
|
return generic_resources
|
||||||
if not isinstance(generic_resources, dict):
|
if not isinstance(generic_resources, dict):
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'generic_resources must be a dict or a list'
|
'generic_resources must be a dict or a list '
|
||||||
' (found {})'.format(type(generic_resources))
|
f'(found {type(generic_resources)})'
|
||||||
)
|
)
|
||||||
resources = []
|
resources = []
|
||||||
for kind, value in six.iteritems(generic_resources):
|
for kind, value in generic_resources.items():
|
||||||
resource_type = None
|
resource_type = None
|
||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
resource_type = 'DiscreteResourceSpec'
|
resource_type = 'DiscreteResourceSpec'
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
resource_type = 'NamedResourceSpec'
|
resource_type = 'NamedResourceSpec'
|
||||||
else:
|
else:
|
||||||
|
kv = {kind: value}
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'Unsupported generic resource reservation '
|
f'Unsupported generic resource reservation type: {kv}'
|
||||||
'type: {}'.format({kind: value})
|
|
||||||
)
|
)
|
||||||
resources.append({
|
resources.append({
|
||||||
resource_type: {'Kind': kind, 'Value': value}
|
resource_type: {'Kind': kind, 'Value': value}
|
||||||
|
@ -384,8 +418,9 @@ class UpdateConfig(dict):
|
||||||
an update before the failure action is invoked, specified as a
|
an update before the failure action is invoked, specified as a
|
||||||
floating point number between 0 and 1. Default: 0
|
floating point number between 0 and 1. Default: 0
|
||||||
order (string): Specifies the order of operations when rolling out an
|
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',
|
def __init__(self, parallelism=0, delay=None, failure_action='continue',
|
||||||
monitor=None, max_failure_ratio=None, order=None):
|
monitor=None, max_failure_ratio=None, order=None):
|
||||||
self['Parallelism'] = parallelism
|
self['Parallelism'] = parallelism
|
||||||
|
@ -421,7 +456,8 @@ class UpdateConfig(dict):
|
||||||
|
|
||||||
class RollbackConfig(UpdateConfig):
|
class RollbackConfig(UpdateConfig):
|
||||||
"""
|
"""
|
||||||
Used to specify the way containe rollbacks should be performed by a service
|
Used to specify the way container rollbacks should be performed by a
|
||||||
|
service
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parallelism (int): Maximum number of tasks to be rolled back in one
|
parallelism (int): Maximum number of tasks to be rolled back in one
|
||||||
|
@ -437,13 +473,13 @@ class RollbackConfig(UpdateConfig):
|
||||||
a rollback before the failure action is invoked, specified as a
|
a rollback before the failure action is invoked, specified as a
|
||||||
floating point number between 0 and 1. Default: 0
|
floating point number between 0 and 1. Default: 0
|
||||||
order (string): Specifies the order of operations when rolling out a
|
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.
|
accepted.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RestartConditionTypesEnum(object):
|
class RestartConditionTypesEnum:
|
||||||
_values = (
|
_values = (
|
||||||
'none',
|
'none',
|
||||||
'on-failure',
|
'on-failure',
|
||||||
|
@ -474,7 +510,7 @@ class RestartPolicy(dict):
|
||||||
max_attempts=0, window=0):
|
max_attempts=0, window=0):
|
||||||
if condition not in self.condition_types._values:
|
if condition not in self.condition_types._values:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Invalid RestartPolicy condition {0}'.format(condition)
|
f'Invalid RestartPolicy condition {condition}'
|
||||||
)
|
)
|
||||||
|
|
||||||
self['Condition'] = condition
|
self['Condition'] = condition
|
||||||
|
@ -496,6 +532,7 @@ class DriverConfig(dict):
|
||||||
name (string): Name of the driver to use.
|
name (string): Name of the driver to use.
|
||||||
options (dict): Driver-specific options. Default: ``None``.
|
options (dict): Driver-specific options. Default: ``None``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, options=None):
|
def __init__(self, name, options=None):
|
||||||
self['Name'] = name
|
self['Name'] = name
|
||||||
if options:
|
if options:
|
||||||
|
@ -517,6 +554,7 @@ class EndpointSpec(dict):
|
||||||
is ``(target_port [, protocol [, publish_mode]])``.
|
is ``(target_port [, protocol [, publish_mode]])``.
|
||||||
Ports can only be provided if the ``vip`` resolution mode is used.
|
Ports can only be provided if the ``vip`` resolution mode is used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mode=None, ports=None):
|
def __init__(self, mode=None, ports=None):
|
||||||
if ports:
|
if ports:
|
||||||
self['Ports'] = convert_service_ports(ports)
|
self['Ports'] = convert_service_ports(ports)
|
||||||
|
@ -533,7 +571,7 @@ def convert_service_ports(ports):
|
||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for k, v in six.iteritems(ports):
|
for k, v in ports.items():
|
||||||
port_spec = {
|
port_spec = {
|
||||||
'Protocol': 'tcp',
|
'Protocol': 'tcp',
|
||||||
'PublishedPort': k
|
'PublishedPort': k
|
||||||
|
@ -559,37 +597,70 @@ def convert_service_ports(ports):
|
||||||
|
|
||||||
class ServiceMode(dict):
|
class ServiceMode(dict):
|
||||||
"""
|
"""
|
||||||
Indicate whether a service should be deployed as a replicated or global
|
Indicate whether a service or a job should be deployed as a replicated
|
||||||
service, and associated parameters
|
or global service, and associated parameters
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode (string): Can be either ``replicated`` or ``global``
|
mode (string): Can be either ``replicated``, ``global``,
|
||||||
|
``replicated-job`` or ``global-job``
|
||||||
replicas (int): Number of replicas. For replicated services only.
|
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):
|
|
||||||
if mode not in ('replicated', 'global'):
|
|
||||||
raise errors.InvalidArgument(
|
|
||||||
'mode must be either "replicated" or "global"'
|
|
||||||
)
|
|
||||||
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:
|
|
||||||
self[mode]['Replicas'] = replicas
|
|
||||||
|
|
||||||
@property
|
def __init__(self, mode, replicas=None, concurrency=None):
|
||||||
def mode(self):
|
replicated_modes = ('replicated', 'replicated-job')
|
||||||
if 'global' in self:
|
supported_modes = replicated_modes + ('global', 'global-job')
|
||||||
return 'global'
|
|
||||||
return 'replicated'
|
if mode not in supported_modes:
|
||||||
|
raise errors.InvalidArgument(
|
||||||
|
'mode must be either "replicated", "global", "replicated-job"'
|
||||||
|
' or "global-job"'
|
||||||
|
)
|
||||||
|
|
||||||
|
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 replicas is not None:
|
||||||
|
if mode == 'replicated':
|
||||||
|
self[service_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
|
@property
|
||||||
def replicas(self):
|
def replicas(self):
|
||||||
if self.mode != 'replicated':
|
if 'replicated' in self:
|
||||||
return None
|
return self['replicated'].get('Replicas')
|
||||||
return self['replicated'].get('Replicas')
|
|
||||||
|
if 'ReplicatedJob' in self:
|
||||||
|
return self['ReplicatedJob'].get('TotalCompletions')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class SecretReference(dict):
|
class SecretReference(dict):
|
||||||
|
@ -663,6 +734,7 @@ class Placement(dict):
|
||||||
platforms (:py:class:`list` of tuple): A list of platforms
|
platforms (:py:class:`list` of tuple): A list of platforms
|
||||||
expressed as ``(arch, os)`` tuples
|
expressed as ``(arch, os)`` tuples
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, constraints=None, preferences=None, platforms=None,
|
def __init__(self, constraints=None, preferences=None, platforms=None,
|
||||||
maxreplicas=None):
|
maxreplicas=None):
|
||||||
if constraints is not None:
|
if constraints is not None:
|
||||||
|
@ -695,11 +767,12 @@ class PlacementPreference(dict):
|
||||||
the scheduler will try to spread tasks evenly over groups of
|
the scheduler will try to spread tasks evenly over groups of
|
||||||
nodes identified by this label.
|
nodes identified by this label.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, strategy, descriptor):
|
def __init__(self, strategy, descriptor):
|
||||||
if strategy != 'spread':
|
if strategy != 'spread':
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
'PlacementPreference strategy value is invalid ({}):'
|
f'PlacementPreference strategy value is invalid ({strategy}): '
|
||||||
' must be "spread".'.format(strategy)
|
'must be "spread".'
|
||||||
)
|
)
|
||||||
self['Spread'] = {'SpreadDescriptor': descriptor}
|
self['Spread'] = {'SpreadDescriptor': descriptor}
|
||||||
|
|
||||||
|
@ -716,6 +789,7 @@ class DNSConfig(dict):
|
||||||
options (:py:class:`list`): A list of internal resolver variables
|
options (:py:class:`list`): A list of internal resolver variables
|
||||||
to be modified (e.g., ``debug``, ``ndots:3``, etc.).
|
to be modified (e.g., ``debug``, ``ndots:3``, etc.).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, nameservers=None, search=None, options=None):
|
def __init__(self, nameservers=None, search=None, options=None):
|
||||||
self['Nameservers'] = nameservers
|
self['Nameservers'] = nameservers
|
||||||
self['Search'] = search
|
self['Search'] = search
|
||||||
|
@ -746,6 +820,7 @@ class Privileges(dict):
|
||||||
selinux_type (string): SELinux type label
|
selinux_type (string): SELinux type label
|
||||||
selinux_level (string): SELinux level label
|
selinux_level (string): SELinux level label
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, credentialspec_file=None, credentialspec_registry=None,
|
def __init__(self, credentialspec_file=None, credentialspec_registry=None,
|
||||||
selinux_disable=None, selinux_user=None, selinux_role=None,
|
selinux_disable=None, selinux_user=None, selinux_role=None,
|
||||||
selinux_type=None, selinux_level=None):
|
selinux_type=None, selinux_level=None):
|
||||||
|
@ -788,6 +863,7 @@ class NetworkAttachmentConfig(dict):
|
||||||
options (:py:class:`dict`): Driver attachment options for the
|
options (:py:class:`dict`): Driver attachment options for the
|
||||||
network target.
|
network target.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, target, aliases=None, options=None):
|
def __init__(self, target, aliases=None, options=None):
|
||||||
self['Target'] = target
|
self['Target'] = target
|
||||||
self['Aliases'] = aliases
|
self['Aliases'] = aliases
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
# flake8: noqa
|
|
||||||
from .build import create_archive, exclude_paths, mkbuildcontext, tar
|
from .build import create_archive, exclude_paths, match_tag, mkbuildcontext, tar
|
||||||
from .decorators import check_resource, minimum_version, update_headers
|
from .decorators import check_resource, minimum_version, update_headers
|
||||||
from .utils import (
|
from .utils import (
|
||||||
compare_version, convert_port_bindings, convert_volume_binds,
|
compare_version,
|
||||||
parse_repository_tag, parse_host,
|
convert_filters,
|
||||||
kwargs_from_env, convert_filters, datetime_to_timestamp,
|
convert_port_bindings,
|
||||||
create_host_config, parse_bytes, parse_env_file, version_lt,
|
convert_service_networks,
|
||||||
version_gte, decode_json_header, split_command, create_ipam_config,
|
convert_volume_binds,
|
||||||
create_ipam_pool, parse_devices, normalize_links, convert_service_networks,
|
create_host_config,
|
||||||
format_environment, format_extra_hosts
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,19 @@ import re
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .fnmatch import fnmatch
|
|
||||||
from ..constants import IS_WINDOWS_PLATFORM
|
from ..constants import IS_WINDOWS_PLATFORM
|
||||||
|
from .fnmatch import fnmatch
|
||||||
|
|
||||||
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
|
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
|
||||||
|
_TAG = re.compile(
|
||||||
|
r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*"
|
||||||
|
r"(?::[0-9]+)?(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"
|
||||||
|
r"(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def match_tag(tag: str) -> bool:
|
||||||
|
return bool(_TAG.match(tag))
|
||||||
|
|
||||||
|
|
||||||
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
||||||
|
@ -44,7 +50,7 @@ def exclude_paths(root, patterns, dockerfile=None):
|
||||||
if dockerfile is None:
|
if dockerfile is None:
|
||||||
dockerfile = 'Dockerfile'
|
dockerfile = 'Dockerfile'
|
||||||
|
|
||||||
patterns.append('!' + dockerfile)
|
patterns.append(f"!{dockerfile}")
|
||||||
pm = PatternMatcher(patterns)
|
pm = PatternMatcher(patterns)
|
||||||
return set(pm.walk(root))
|
return set(pm.walk(root))
|
||||||
|
|
||||||
|
@ -69,7 +75,7 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
|
||||||
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
|
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
|
||||||
if files is None:
|
if files is None:
|
||||||
files = build_file_list(root)
|
files = build_file_list(root)
|
||||||
extra_names = set(e[0] for e in extra_files)
|
extra_names = {e[0] for e in extra_files}
|
||||||
for path in files:
|
for path in files:
|
||||||
if path in extra_names:
|
if path in extra_names:
|
||||||
# Extra files override context files with the same name
|
# Extra files override context files with the same name
|
||||||
|
@ -95,10 +101,10 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
|
||||||
try:
|
try:
|
||||||
with open(full_path, 'rb') as f:
|
with open(full_path, 'rb') as f:
|
||||||
t.addfile(i, f)
|
t.addfile(i, f)
|
||||||
except IOError:
|
except OSError as oe:
|
||||||
raise IOError(
|
raise OSError(
|
||||||
'Can not read file in context: {}'.format(full_path)
|
f'Can not read file in context: {full_path}'
|
||||||
)
|
) from oe
|
||||||
else:
|
else:
|
||||||
# Directories, FIFOs, symlinks... don't need to be read.
|
# Directories, FIFOs, symlinks... don't need to be read.
|
||||||
t.addfile(i, None)
|
t.addfile(i, None)
|
||||||
|
@ -119,12 +125,8 @@ def mkbuildcontext(dockerfile):
|
||||||
t = tarfile.open(mode='w', fileobj=f)
|
t = tarfile.open(mode='w', fileobj=f)
|
||||||
if isinstance(dockerfile, io.StringIO):
|
if isinstance(dockerfile, io.StringIO):
|
||||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||||
if six.PY3:
|
raise TypeError('Please use io.BytesIO to create in-memory '
|
||||||
raise TypeError('Please use io.BytesIO to create in-memory '
|
'Dockerfiles with Python 3')
|
||||||
'Dockerfiles with Python 3')
|
|
||||||
else:
|
|
||||||
dfinfo.size = len(dockerfile.getvalue())
|
|
||||||
dockerfile.seek(0)
|
|
||||||
elif isinstance(dockerfile, io.BytesIO):
|
elif isinstance(dockerfile, io.BytesIO):
|
||||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||||
dfinfo.size = len(dockerfile.getvalue())
|
dfinfo.size = len(dockerfile.getvalue())
|
||||||
|
@ -154,7 +156,7 @@ def walk(root, patterns, default=True):
|
||||||
|
|
||||||
# Heavily based on
|
# Heavily based on
|
||||||
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
|
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
|
||||||
class PatternMatcher(object):
|
class PatternMatcher:
|
||||||
def __init__(self, patterns):
|
def __init__(self, patterns):
|
||||||
self.patterns = list(filter(
|
self.patterns = list(filter(
|
||||||
lambda p: p.dirs, [Pattern(p) for p in patterns]
|
lambda p: p.dirs, [Pattern(p) for p in patterns]
|
||||||
|
@ -186,7 +188,7 @@ class PatternMatcher(object):
|
||||||
fpath = os.path.join(
|
fpath = os.path.join(
|
||||||
os.path.relpath(current_dir, root), f
|
os.path.relpath(current_dir, root), f
|
||||||
)
|
)
|
||||||
if fpath.startswith('.' + os.path.sep):
|
if fpath.startswith(f".{os.path.sep}"):
|
||||||
fpath = fpath[2:]
|
fpath = fpath[2:]
|
||||||
match = self.matches(fpath)
|
match = self.matches(fpath)
|
||||||
if not match:
|
if not match:
|
||||||
|
@ -212,13 +214,12 @@ class PatternMatcher(object):
|
||||||
break
|
break
|
||||||
if skip:
|
if skip:
|
||||||
continue
|
continue
|
||||||
for sub in rec_walk(cur):
|
yield from rec_walk(cur)
|
||||||
yield sub
|
|
||||||
|
|
||||||
return rec_walk(root)
|
return rec_walk(root)
|
||||||
|
|
||||||
|
|
||||||
class Pattern(object):
|
class Pattern:
|
||||||
def __init__(self, pattern_str):
|
def __init__(self, pattern_str):
|
||||||
self.exclusion = False
|
self.exclusion = False
|
||||||
if pattern_str.startswith('!'):
|
if pattern_str.startswith('!'):
|
||||||
|
@ -231,6 +232,9 @@ class Pattern(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize(cls, p):
|
def normalize(cls, p):
|
||||||
|
|
||||||
|
# Remove trailing spaces
|
||||||
|
p = p.strip()
|
||||||
|
|
||||||
# Leading and trailing slashes are not relevant. Yes,
|
# Leading and trailing slashes are not relevant. Yes,
|
||||||
# "foo.py/" must exclude the "foo.py" regular file. "."
|
# "foo.py/" must exclude the "foo.py" regular file. "."
|
||||||
# components are not relevant either, even if the whole
|
# components are not relevant either, even if the whole
|
||||||
|
|
|
@ -18,11 +18,11 @@ def find_config_file(config_path=None):
|
||||||
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
|
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
|
||||||
]))
|
]))
|
||||||
|
|
||||||
log.debug("Trying paths: {0}".format(repr(paths)))
|
log.debug(f"Trying paths: {repr(paths)}")
|
||||||
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
log.debug("Found file at path: {0}".format(path))
|
log.debug(f"Found file at path: {path}")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
log.debug("No config file found")
|
log.debug("No config file found")
|
||||||
|
@ -57,7 +57,7 @@ def load_general_config(config_path=None):
|
||||||
try:
|
try:
|
||||||
with open(config_file) as f:
|
with open(config_file) as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except (IOError, ValueError) as e:
|
except (OSError, ValueError) as e:
|
||||||
# In the case of a legacy `.dockercfg` file, we won't
|
# In the case of a legacy `.dockercfg` file, we won't
|
||||||
# be able to load any JSON data.
|
# be able to load any JSON data.
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
|
|
|
@ -27,9 +27,7 @@ def minimum_version(version):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
if utils.version_lt(self._version, version):
|
if utils.version_lt(self._version, version):
|
||||||
raise errors.InvalidVersion(
|
raise errors.InvalidVersion(
|
||||||
'{0} is not available for version < {1}'.format(
|
f'{f.__name__} is not available for version < {version}',
|
||||||
f.__name__, version
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
@ -79,18 +79,18 @@ def translate(pat):
|
||||||
i = i + 1
|
i = i + 1
|
||||||
if i >= n:
|
if i >= n:
|
||||||
# is "**EOF" - to align with .gitignore just accept all
|
# is "**EOF" - to align with .gitignore just accept all
|
||||||
res = res + '.*'
|
res = f"{res}.*"
|
||||||
else:
|
else:
|
||||||
# is "**"
|
# is "**"
|
||||||
# Note that this allows for any # of /'s (even 0) because
|
# Note that this allows for any # of /'s (even 0) because
|
||||||
# the .* will eat everything, even /'s
|
# the .* will eat everything, even /'s
|
||||||
res = res + '(.*/)?'
|
res = f"{res}(.*/)?"
|
||||||
else:
|
else:
|
||||||
# is "*" so map it to anything but "/"
|
# is "*" so map it to anything but "/"
|
||||||
res = res + '[^/]*'
|
res = f"{res}[^/]*"
|
||||||
elif c == '?':
|
elif c == '?':
|
||||||
# "?" is any char except "/"
|
# "?" is any char except "/"
|
||||||
res = res + '[^/]'
|
res = f"{res}[^/]"
|
||||||
elif c == '[':
|
elif c == '[':
|
||||||
j = i
|
j = i
|
||||||
if j < n and pat[j] == '!':
|
if j < n and pat[j] == '!':
|
||||||
|
@ -100,16 +100,16 @@ def translate(pat):
|
||||||
while j < n and pat[j] != ']':
|
while j < n and pat[j] != ']':
|
||||||
j = j + 1
|
j = j + 1
|
||||||
if j >= n:
|
if j >= n:
|
||||||
res = res + '\\['
|
res = f"{res}\\["
|
||||||
else:
|
else:
|
||||||
stuff = pat[i:j].replace('\\', '\\\\')
|
stuff = pat[i:j].replace('\\', '\\\\')
|
||||||
i = j + 1
|
i = j + 1
|
||||||
if stuff[0] == '!':
|
if stuff[0] == '!':
|
||||||
stuff = '^' + stuff[1:]
|
stuff = f"^{stuff[1:]}"
|
||||||
elif stuff[0] == '^':
|
elif stuff[0] == '^':
|
||||||
stuff = '\\' + stuff
|
stuff = f"\\{stuff}"
|
||||||
res = '%s[%s]' % (res, stuff)
|
res = f'{res}[{stuff}]'
|
||||||
else:
|
else:
|
||||||
res = res + re.escape(c)
|
res = res + re.escape(c)
|
||||||
|
|
||||||
return res + '$'
|
return f"{res}$"
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import json.decoder
|
import json.decoder
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from ..errors import StreamParseError
|
from ..errors import StreamParseError
|
||||||
|
|
||||||
|
|
||||||
json_decoder = json.JSONDecoder()
|
json_decoder = json.JSONDecoder()
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +14,7 @@ def stream_as_text(stream):
|
||||||
instead of byte streams.
|
instead of byte streams.
|
||||||
"""
|
"""
|
||||||
for data in stream:
|
for data in stream:
|
||||||
if not isinstance(data, six.text_type):
|
if not isinstance(data, str):
|
||||||
data = data.decode('utf-8', 'replace')
|
data = data.decode('utf-8', 'replace')
|
||||||
yield data
|
yield data
|
||||||
|
|
||||||
|
@ -46,8 +40,8 @@ def json_stream(stream):
|
||||||
return split_buffer(stream, json_splitter, json_decoder.decode)
|
return split_buffer(stream, json_splitter, json_decoder.decode)
|
||||||
|
|
||||||
|
|
||||||
def line_splitter(buffer, separator=u'\n'):
|
def line_splitter(buffer, separator='\n'):
|
||||||
index = buffer.find(six.text_type(separator))
|
index = buffer.find(str(separator))
|
||||||
if index == -1:
|
if index == -1:
|
||||||
return None
|
return None
|
||||||
return buffer[:index + 1], buffer[index + 1:]
|
return buffer[:index + 1], buffer[index + 1:]
|
||||||
|
@ -61,7 +55,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
|
||||||
of the input.
|
of the input.
|
||||||
"""
|
"""
|
||||||
splitter = splitter or line_splitter
|
splitter = splitter or line_splitter
|
||||||
buffered = six.text_type('')
|
buffered = ''
|
||||||
|
|
||||||
for data in stream_as_text(stream):
|
for data in stream_as_text(stream):
|
||||||
buffered += data
|
buffered += data
|
||||||
|
@ -77,4 +71,4 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
|
||||||
try:
|
try:
|
||||||
yield decoder(buffered)
|
yield decoder(buffered)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise StreamParseError(e)
|
raise StreamParseError(e) from e
|
||||||
|
|
|
@ -49,7 +49,7 @@ def port_range(start, end, proto, randomly_available_port=False):
|
||||||
if not end:
|
if not end:
|
||||||
return [start + proto]
|
return [start + proto]
|
||||||
if randomly_available_port:
|
if randomly_available_port:
|
||||||
return ['{}-{}'.format(start, end) + proto]
|
return [f"{start}-{end}{proto}"]
|
||||||
return [str(port) + proto for port in range(int(start), int(end) + 1)]
|
return [str(port) + proto for port in range(int(start), int(end) + 1)]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -69,5 +69,9 @@ class ProxyConfig(dict):
|
||||||
return proxy_env + environment
|
return proxy_env + environment
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'ProxyConfig(http={}, https={}, ftp={}, no_proxy={})'.format(
|
return (
|
||||||
self.http, self.https, self.ftp, self.no_proxy)
|
'ProxyConfig('
|
||||||
|
f'http={self.http}, https={self.https}, '
|
||||||
|
f'ftp={self.ftp}, no_proxy={self.no_proxy}'
|
||||||
|
')'
|
||||||
|
)
|
||||||
|
|
|
@ -4,8 +4,6 @@ import select
|
||||||
import socket as pysocket
|
import socket as pysocket
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ..transport import NpipeSocket
|
from ..transport import NpipeSocket
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -20,6 +18,11 @@ class SocketError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# NpipeSockets have their own error types
|
||||||
|
# pywintypes.error: (109, 'ReadFile', 'The pipe has been ended.')
|
||||||
|
NPIPE_ENDED = 109
|
||||||
|
|
||||||
|
|
||||||
def read(socket, n=4096):
|
def read(socket, n=4096):
|
||||||
"""
|
"""
|
||||||
Reads at most n bytes from socket
|
Reads at most n bytes from socket
|
||||||
|
@ -27,18 +30,33 @@ def read(socket, n=4096):
|
||||||
|
|
||||||
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
|
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
|
||||||
|
|
||||||
if six.PY3 and not isinstance(socket, NpipeSocket):
|
if not isinstance(socket, NpipeSocket):
|
||||||
select.select([socket], [], [])
|
if not hasattr(select, "poll"):
|
||||||
|
# Limited to 1024
|
||||||
|
select.select([socket], [], [])
|
||||||
|
else:
|
||||||
|
poll = select.poll()
|
||||||
|
poll.register(socket, select.POLLIN | select.POLLPRI)
|
||||||
|
poll.poll()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(socket, 'recv'):
|
if hasattr(socket, 'recv'):
|
||||||
return socket.recv(n)
|
return socket.recv(n)
|
||||||
if six.PY3 and isinstance(socket, getattr(pysocket, 'SocketIO')):
|
if isinstance(socket, pysocket.SocketIO):
|
||||||
return socket.read(n)
|
return socket.read(n)
|
||||||
return os.read(socket.fileno(), n)
|
return os.read(socket.fileno(), n)
|
||||||
except EnvironmentError as e:
|
except OSError as e:
|
||||||
if e.errno not in recoverable_errors:
|
if e.errno not in recoverable_errors:
|
||||||
raise
|
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):
|
def read_exactly(socket, n):
|
||||||
|
@ -46,7 +64,7 @@ def read_exactly(socket, n):
|
||||||
Reads exactly n bytes from socket
|
Reads exactly n bytes from socket
|
||||||
Raises SocketError if there isn't enough data
|
Raises SocketError if there isn't enough data
|
||||||
"""
|
"""
|
||||||
data = six.binary_type()
|
data = b""
|
||||||
while len(data) < n:
|
while len(data) < n:
|
||||||
next_data = read(socket, n - len(data))
|
next_data = read(socket, n - len(data))
|
||||||
if not next_data:
|
if not next_data:
|
||||||
|
@ -134,7 +152,7 @@ def consume_socket_output(frames, demux=False):
|
||||||
if demux is False:
|
if demux is False:
|
||||||
# If the streams are multiplexed, the generator returns strings, that
|
# If the streams are multiplexed, the generator returns strings, that
|
||||||
# we just need to concatenate.
|
# we just need to concatenate.
|
||||||
return six.binary_type().join(frames)
|
return b"".join(frames)
|
||||||
|
|
||||||
# If the streams are demultiplexed, the generator yields tuples
|
# If the streams are demultiplexed, the generator yields tuples
|
||||||
# (stdout, stderr)
|
# (stdout, stderr)
|
||||||
|
@ -166,4 +184,4 @@ def demux_adaptor(stream_id, data):
|
||||||
elif stream_id == STDERR:
|
elif stream_id == STDERR:
|
||||||
return (None, data)
|
return (None, data)
|
||||||
else:
|
else:
|
||||||
raise ValueError('{0} is not a valid stream'.format(stream_id))
|
raise ValueError(f'{stream_id} is not a valid stream')
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
import base64
|
import base64
|
||||||
|
import collections
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import shlex
|
import shlex
|
||||||
import string
|
import string
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from distutils.version import StrictVersion
|
from functools import lru_cache
|
||||||
|
from itertools import zip_longest
|
||||||
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from .. import tls
|
from ..constants import (
|
||||||
from ..constants import DEFAULT_HTTP_HOST
|
BYTE_UNITS,
|
||||||
from ..constants import DEFAULT_UNIX_SOCKET
|
DEFAULT_HTTP_HOST,
|
||||||
from ..constants import DEFAULT_NPIPE
|
DEFAULT_NPIPE,
|
||||||
from ..constants import BYTE_UNITS
|
DEFAULT_UNIX_SOCKET,
|
||||||
|
)
|
||||||
|
from ..tls import TLSConfig
|
||||||
|
|
||||||
from urllib.parse import splitnport, urlparse
|
URLComponents = collections.namedtuple(
|
||||||
|
'URLComponents',
|
||||||
|
'scheme netloc url params query fragment',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_ipam_pool(*args, **kwargs):
|
def create_ipam_pool(*args, **kwargs):
|
||||||
|
@ -37,6 +45,7 @@ def decode_json_header(header):
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
def compare_version(v1, v2):
|
def compare_version(v1, v2):
|
||||||
"""Compare docker versions
|
"""Compare docker versions
|
||||||
|
|
||||||
|
@ -49,14 +58,20 @@ def compare_version(v1, v2):
|
||||||
>>> compare_version(v2, v2)
|
>>> compare_version(v2, v2)
|
||||||
0
|
0
|
||||||
"""
|
"""
|
||||||
s1 = StrictVersion(v1)
|
if v1 == v2:
|
||||||
s2 = StrictVersion(v2)
|
|
||||||
if s1 == s2:
|
|
||||||
return 0
|
return 0
|
||||||
elif s1 > s2:
|
# Split into `sys.version_info` like tuples.
|
||||||
return -1
|
s1 = tuple(int(p) for p in v1.split('.'))
|
||||||
else:
|
s2 = tuple(int(p) for p in v2.split('.'))
|
||||||
return 1
|
# 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
|
||||||
|
|
||||||
|
|
||||||
def version_lt(v1, v2):
|
def version_lt(v1, v2):
|
||||||
|
@ -120,8 +135,7 @@ def convert_volume_binds(binds):
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
if 'ro' in v and 'mode' in v:
|
if 'ro' in v and 'mode' in v:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Binding cannot contain both "ro" and "mode": {}'
|
f'Binding cannot contain both "ro" and "mode": {v!r}'
|
||||||
.format(repr(v))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bind = v['bind']
|
bind = v['bind']
|
||||||
|
@ -135,14 +149,30 @@ def convert_volume_binds(binds):
|
||||||
else:
|
else:
|
||||||
mode = 'rw'
|
mode = 'rw'
|
||||||
|
|
||||||
|
# NOTE: this is only relevant for Linux hosts
|
||||||
|
# (doesn't apply in Docker Desktop)
|
||||||
|
propagation_modes = [
|
||||||
|
'rshared',
|
||||||
|
'shared',
|
||||||
|
'rslave',
|
||||||
|
'slave',
|
||||||
|
'rprivate',
|
||||||
|
'private',
|
||||||
|
]
|
||||||
|
if 'propagation' in v and v['propagation'] in propagation_modes:
|
||||||
|
if mode:
|
||||||
|
mode = f"{mode},{v['propagation']}"
|
||||||
|
else:
|
||||||
|
mode = v['propagation']
|
||||||
|
|
||||||
result.append(
|
result.append(
|
||||||
str('{0}:{1}:{2}').format(k, bind, mode)
|
f'{k}:{bind}:{mode}'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if isinstance(v, bytes):
|
if isinstance(v, bytes):
|
||||||
v = v.decode('utf-8')
|
v = v.decode('utf-8')
|
||||||
result.append(
|
result.append(
|
||||||
str('{0}:{1}:rw').format(k, v)
|
f'{k}:{v}:rw'
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -153,8 +183,8 @@ def convert_tmpfs_mounts(tmpfs):
|
||||||
|
|
||||||
if not isinstance(tmpfs, list):
|
if not isinstance(tmpfs, list):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Expected tmpfs value to be either a list or a dict, found: {}'
|
'Expected tmpfs value to be either a list or a dict, '
|
||||||
.format(type(tmpfs).__name__)
|
f'found: {type(tmpfs).__name__}'
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
@ -168,8 +198,8 @@ def convert_tmpfs_mounts(tmpfs):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Expected item in tmpfs list to be a string, found: {}"
|
"Expected item in tmpfs list to be a string, "
|
||||||
.format(type(mount).__name__)
|
f"found: {type(mount).__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
result[name] = options
|
result[name] = options
|
||||||
|
@ -201,10 +231,6 @@ def parse_repository_tag(repo_name):
|
||||||
|
|
||||||
|
|
||||||
def parse_host(addr, is_win32=False, tls=False):
|
def parse_host(addr, is_win32=False, tls=False):
|
||||||
path = ''
|
|
||||||
port = None
|
|
||||||
host = None
|
|
||||||
|
|
||||||
# Sensible defaults
|
# Sensible defaults
|
||||||
if not addr and is_win32:
|
if not addr and is_win32:
|
||||||
return DEFAULT_NPIPE
|
return DEFAULT_NPIPE
|
||||||
|
@ -215,9 +241,9 @@ def parse_host(addr, is_win32=False, tls=False):
|
||||||
|
|
||||||
parsed_url = urlparse(addr)
|
parsed_url = urlparse(addr)
|
||||||
proto = parsed_url.scheme
|
proto = parsed_url.scheme
|
||||||
if not proto or any([x not in string.ascii_letters + '+' for x in proto]):
|
if not proto or any(x not in f"{string.ascii_letters}+" for x in proto):
|
||||||
# https://bugs.python.org/issue754016
|
# https://bugs.python.org/issue754016
|
||||||
parsed_url = urlparse('//' + addr, 'tcp')
|
parsed_url = urlparse(f"//{addr}", 'tcp')
|
||||||
proto = 'tcp'
|
proto = 'tcp'
|
||||||
|
|
||||||
if proto == 'fd':
|
if proto == 'fd':
|
||||||
|
@ -233,14 +259,14 @@ def parse_host(addr, is_win32=False, tls=False):
|
||||||
|
|
||||||
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
|
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
"Invalid bind address protocol: {}".format(addr)
|
f"Invalid bind address protocol: {addr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if proto == 'tcp' and not parsed_url.netloc:
|
if proto == 'tcp' and not parsed_url.netloc:
|
||||||
# "tcp://" is exceptionally disallowed by convention;
|
# "tcp://" is exceptionally disallowed by convention;
|
||||||
# omitting a hostname for other protocols is fine
|
# omitting a hostname for other protocols is fine
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid bind address format: {}'.format(addr)
|
f'Invalid bind address format: {addr}'
|
||||||
)
|
)
|
||||||
|
|
||||||
if any([
|
if any([
|
||||||
|
@ -248,45 +274,51 @@ def parse_host(addr, is_win32=False, tls=False):
|
||||||
parsed_url.password
|
parsed_url.password
|
||||||
]):
|
]):
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid bind address format: {}'.format(addr)
|
f'Invalid bind address format: {addr}'
|
||||||
)
|
)
|
||||||
|
|
||||||
if parsed_url.path and proto == 'ssh':
|
if parsed_url.path and proto == 'ssh':
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid bind address format: no path allowed for this protocol:'
|
f'Invalid bind address format: no path allowed for this protocol: {addr}'
|
||||||
' {}'.format(addr)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
path = parsed_url.path
|
path = parsed_url.path
|
||||||
if proto == 'unix' and parsed_url.hostname is not None:
|
if proto == 'unix' and parsed_url.hostname is not None:
|
||||||
# For legacy reasons, we consider unix://path
|
# For legacy reasons, we consider unix://path
|
||||||
# to be valid and equivalent to unix:///path
|
# to be valid and equivalent to unix:///path
|
||||||
path = '/'.join((parsed_url.hostname, path))
|
path = f"{parsed_url.hostname}/{path}"
|
||||||
|
|
||||||
|
netloc = parsed_url.netloc
|
||||||
if proto in ('tcp', 'ssh'):
|
if proto in ('tcp', 'ssh'):
|
||||||
# parsed_url.hostname strips brackets from IPv6 addresses,
|
port = parsed_url.port or 0
|
||||||
# which can be problematic hence our use of splitnport() instead.
|
if port <= 0:
|
||||||
host, port = splitnport(parsed_url.netloc)
|
|
||||||
if port is None or port < 0:
|
|
||||||
if proto != 'ssh':
|
if proto != 'ssh':
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid bind address format: port is required:'
|
f'Invalid bind address format: port is required: {addr}'
|
||||||
' {}'.format(addr)
|
|
||||||
)
|
)
|
||||||
port = 22
|
port = 22
|
||||||
|
netloc = f'{parsed_url.netloc}:{port}'
|
||||||
|
|
||||||
if not host:
|
if not parsed_url.hostname:
|
||||||
host = DEFAULT_HTTP_HOST
|
netloc = f'{DEFAULT_HTTP_HOST}:{port}'
|
||||||
|
|
||||||
# Rewrite schemes to fit library internals (requests adapters)
|
# Rewrite schemes to fit library internals (requests adapters)
|
||||||
if proto == 'tcp':
|
if proto == 'tcp':
|
||||||
proto = 'http{}'.format('s' if tls else '')
|
proto = f"http{'s' if tls else ''}"
|
||||||
elif proto == 'unix':
|
elif proto == 'unix':
|
||||||
proto = 'http+unix'
|
proto = 'http+unix'
|
||||||
|
|
||||||
if proto in ('http+unix', 'npipe'):
|
if proto in ('http+unix', 'npipe'):
|
||||||
return "{}://{}".format(proto, path).rstrip('/')
|
return f"{proto}://{path}".rstrip('/')
|
||||||
return '{0}://{1}:{2}{3}'.format(proto, host, port, path).rstrip('/')
|
|
||||||
|
return urlunparse(URLComponents(
|
||||||
|
scheme=proto,
|
||||||
|
netloc=netloc,
|
||||||
|
url=path,
|
||||||
|
params='',
|
||||||
|
query='',
|
||||||
|
fragment='',
|
||||||
|
)).rstrip('/')
|
||||||
|
|
||||||
|
|
||||||
def parse_devices(devices):
|
def parse_devices(devices):
|
||||||
|
@ -297,7 +329,7 @@ def parse_devices(devices):
|
||||||
continue
|
continue
|
||||||
if not isinstance(device, str):
|
if not isinstance(device, str):
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid device type {0}'.format(type(device))
|
f'Invalid device type {type(device)}'
|
||||||
)
|
)
|
||||||
device_mapping = device.split(':')
|
device_mapping = device.split(':')
|
||||||
if device_mapping:
|
if device_mapping:
|
||||||
|
@ -318,7 +350,7 @@ def parse_devices(devices):
|
||||||
return device_list
|
return device_list
|
||||||
|
|
||||||
|
|
||||||
def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
|
def kwargs_from_env(environment=None):
|
||||||
if not environment:
|
if not environment:
|
||||||
environment = os.environ
|
environment = os.environ
|
||||||
host = environment.get('DOCKER_HOST')
|
host = environment.get('DOCKER_HOST')
|
||||||
|
@ -346,18 +378,11 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
|
||||||
if not cert_path:
|
if not cert_path:
|
||||||
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
|
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
|
||||||
|
|
||||||
if not tls_verify and assert_hostname is None:
|
params['tls'] = TLSConfig(
|
||||||
# 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'),
|
client_cert=(os.path.join(cert_path, 'cert.pem'),
|
||||||
os.path.join(cert_path, 'key.pem')),
|
os.path.join(cert_path, 'key.pem')),
|
||||||
ca_cert=os.path.join(cert_path, 'ca.pem'),
|
ca_cert=os.path.join(cert_path, 'ca.pem'),
|
||||||
verify=tls_verify,
|
verify=tls_verify,
|
||||||
ssl_version=ssl_version,
|
|
||||||
assert_hostname=assert_hostname,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
@ -378,8 +403,8 @@ def convert_filters(filters):
|
||||||
|
|
||||||
|
|
||||||
def datetime_to_timestamp(dt):
|
def datetime_to_timestamp(dt):
|
||||||
"""Convert a UTC datetime to a Unix timestamp"""
|
"""Convert a datetime to a Unix timestamp"""
|
||||||
delta = dt - datetime.utcfromtimestamp(0)
|
delta = dt.astimezone(timezone.utc) - datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||||
return delta.seconds + delta.days * 24 * 3600
|
return delta.seconds + delta.days * 24 * 3600
|
||||||
|
|
||||||
|
|
||||||
|
@ -406,19 +431,18 @@ def parse_bytes(s):
|
||||||
if suffix in units.keys() or suffix.isdigit():
|
if suffix in units.keys() or suffix.isdigit():
|
||||||
try:
|
try:
|
||||||
digits = float(digits_part)
|
digits = float(digits_part)
|
||||||
except ValueError:
|
except ValueError as ve:
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Failed converting the string value for memory ({0}) to'
|
'Failed converting the string value for memory '
|
||||||
' an integer.'.format(digits_part)
|
f'({digits_part}) to an integer.'
|
||||||
)
|
) from ve
|
||||||
|
|
||||||
# Reconvert to long for the final result
|
# Reconvert to long for the final result
|
||||||
s = int(digits * units[suffix])
|
s = int(digits * units[suffix])
|
||||||
else:
|
else:
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'The specified value for memory ({0}) should specify the'
|
f'The specified value for memory ({s}) should specify the units. '
|
||||||
' units. The postfix should be one of the `b` `k` `m` `g`'
|
'The postfix should be one of the `b` `k` `m` `g` characters'
|
||||||
' characters'.format(s)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
@ -428,7 +452,7 @@ def normalize_links(links):
|
||||||
if isinstance(links, dict):
|
if isinstance(links, dict):
|
||||||
links = iter(links.items())
|
links = iter(links.items())
|
||||||
|
|
||||||
return ['{0}:{1}'.format(k, v) if v else k for k, v in sorted(links)]
|
return [f'{k}:{v}' if v else k for k, v in sorted(links)]
|
||||||
|
|
||||||
|
|
||||||
def parse_env_file(env_file):
|
def parse_env_file(env_file):
|
||||||
|
@ -438,7 +462,7 @@ def parse_env_file(env_file):
|
||||||
"""
|
"""
|
||||||
environment = {}
|
environment = {}
|
||||||
|
|
||||||
with open(env_file, 'r') as f:
|
with open(env_file) as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
|
|
||||||
if line[0] == '#':
|
if line[0] == '#':
|
||||||
|
@ -454,8 +478,7 @@ def parse_env_file(env_file):
|
||||||
environment[k] = v
|
environment[k] = v
|
||||||
else:
|
else:
|
||||||
raise errors.DockerException(
|
raise errors.DockerException(
|
||||||
'Invalid line in environment file {0}:\n{1}'.format(
|
f'Invalid line in environment file {env_file}:\n{line}')
|
||||||
env_file, line))
|
|
||||||
|
|
||||||
return environment
|
return environment
|
||||||
|
|
||||||
|
@ -471,7 +494,7 @@ def format_environment(environment):
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
value = value.decode('utf-8')
|
value = value.decode('utf-8')
|
||||||
|
|
||||||
return u'{key}={value}'.format(key=key, value=value)
|
return f'{key}={value}'
|
||||||
return [format_env(*var) for var in iter(environment.items())]
|
return [format_env(*var) for var in iter(environment.items())]
|
||||||
|
|
||||||
|
|
||||||
|
@ -479,11 +502,11 @@ def format_extra_hosts(extra_hosts, task=False):
|
||||||
# Use format dictated by Swarm API if container is part of a task
|
# Use format dictated by Swarm API if container is part of a task
|
||||||
if task:
|
if task:
|
||||||
return [
|
return [
|
||||||
'{} {}'.format(v, k) for k, v in sorted(iter(extra_hosts.items()))
|
f'{v} {k}' for k, v in sorted(iter(extra_hosts.items()))
|
||||||
]
|
]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'{}:{}'.format(k, v) for k, v in sorted(iter(extra_hosts.items()))
|
f'{k}:{v}' for k, v in sorted(iter(extra_hosts.items()))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
version = "4.5.0-dev"
|
try:
|
||||||
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])
|
from ._version import __version__
|
||||||
|
except ImportError:
|
||||||
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
try:
|
||||||
|
__version__ = version('docker')
|
||||||
|
except PackageNotFoundError:
|
||||||
|
__version__ = '0.0.0'
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
recommonmark==0.4.0
|
|
||||||
Sphinx==1.4.6
|
|
|
@ -1,3 +1,12 @@
|
||||||
dl.hide-signature > dt {
|
dl.hide-signature > dt {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dl.field-list > dt {
|
||||||
|
/* prevent code blocks from forcing wrapping on the "Parameters" header */
|
||||||
|
word-break: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.literal{
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,197 @@
|
||||||
Change log
|
Changelog
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
7.1.0
|
||||||
|
-----
|
||||||
|
### Upgrade Notes
|
||||||
|
- Bumped minimum engine API version to 1.24
|
||||||
|
- Bumped default engine API version to 1.44 (Moby 25.0)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed issue with tag parsing when the registry address includes ports that resulted in `invalid tag format` errors
|
||||||
|
- Fixed issue preventing creating new configs (`ConfigCollection`), which failed with a `KeyError` due to the `name` field
|
||||||
|
- Fixed an issue due to an update in the [requests](https://github.com/psf/requests) package breaking `docker-py` by applying the [suggested fix](https://github.com/psf/requests/pull/6710)
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
- Documentation improvements
|
||||||
|
- Updated Ruff (linter) and fixed minor linting issues
|
||||||
|
- Packaging/CI updates
|
||||||
|
- Started using hatch for packaging (https://github.com/pypa/hatch)
|
||||||
|
- Updated `setup-python` github action
|
||||||
|
- Updated tests
|
||||||
|
- Stopped checking for deprecated container and image related fields (`Container` and `ContainerConfig`)
|
||||||
|
- Updated tests that check `NetworkSettings.Networks.<network>.Aliases` due to engine changes
|
||||||
|
|
||||||
|
7.0.0
|
||||||
|
-----
|
||||||
|
### Upgrade Notes
|
||||||
|
- Removed SSL version (`ssl_version`) and explicit hostname check (`assert_hostname`) options
|
||||||
|
- `assert_hostname` has not been used since Python 3.6 and was removed in 3.12
|
||||||
|
- Python 3.7+ supports TLSv1.3 by default
|
||||||
|
- Websocket support is no longer included by default
|
||||||
|
- Use `pip install docker[websockets]` to include `websocket-client` dependency
|
||||||
|
- By default, `docker-py` hijacks the TCP connection and does not use Websockets
|
||||||
|
- Websocket client is only required to use `attach_socket(container, ws=True)`
|
||||||
|
- Python 3.7 no longer officially supported (reached end-of-life June 2023)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Python 3.12 support
|
||||||
|
- Full `networking_config` support for `containers.create()`
|
||||||
|
- Replaces `network_driver_opt` (added in 6.1.0)
|
||||||
|
- Add `health()` property to container that returns status (e.g. `unhealthy`)
|
||||||
|
- Add `pause` option to `container.commit()`
|
||||||
|
- Add support for bind mount propagation (e.g. `rshared`, `private`)
|
||||||
|
- Add `filters`, `keep_storage`, and `all` parameters to `prune_builds()` (requires API v1.39+)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Consistently return `docker.errors.NotFound` on 404 responses
|
||||||
|
- Validate tag format before image push
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
- Upgraded urllib3 version in `requirements.txt` (used for development/tests)
|
||||||
|
- Documentation typo fixes & formatting improvements
|
||||||
|
- Fixed integration test compatibility for newer Moby engine versions
|
||||||
|
- Switch to [ruff](https://github.com/astral-sh/ruff) for linting
|
||||||
|
|
||||||
|
6.1.3
|
||||||
|
-----
|
||||||
|
#### Bugfixes
|
||||||
|
- Fix compatibility with [`eventlet/eventlet`](https://github.com/eventlet/eventlet)
|
||||||
|
|
||||||
|
6.1.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
#### Bugfixes
|
||||||
|
- Fix for socket timeouts on long `docker exec` calls
|
||||||
|
|
||||||
|
6.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
#### Bugfixes
|
||||||
|
- Fix `containers.stats()` hanging with `stream=True`
|
||||||
|
- Correct return type in docs for `containers.diff()` method
|
||||||
|
|
||||||
|
|
||||||
|
6.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Upgrade Notes
|
||||||
|
- Errors are no longer returned during client initialization if the credential helper cannot be found. A warning will be emitted instead, and an error is returned if the credential helper is used.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Python 3.11 support
|
||||||
|
- Use `poll()` instead of `select()` on non-Windows platforms
|
||||||
|
- New API fields
|
||||||
|
- `network_driver_opt` on container run / create
|
||||||
|
- `one-shot` on container stats
|
||||||
|
- `status` on services list
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Support for requests 2.29.0+ and urllib3 2.x
|
||||||
|
- Do not strip characters from volume names
|
||||||
|
- Fix connection leak on container.exec_* operations
|
||||||
|
- Fix errors closing named pipes on Windows
|
||||||
|
|
||||||
|
6.0.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Fix for `The pipe has been ended errors` on Windows
|
||||||
|
- Support floats for container log filtering by timestamp (`since` / `until`)
|
||||||
|
|
||||||
|
6.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
### 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
|
4.4.4
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
44
docs/conf.py
44
docs/conf.py
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# docker-sdk-python documentation build configuration file, created by
|
# docker-sdk-python documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Wed Sep 14 15:48:58 2016.
|
# sphinx-quickstart on Wed Sep 14 15:48:58 2016.
|
||||||
|
@ -19,6 +18,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,24 +35,19 @@ sys.path.insert(0, os.path.abspath('..'))
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.napoleon',
|
'sphinx.ext.napoleon',
|
||||||
|
'myst_parser'
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
|
||||||
from recommonmark.parser import CommonMarkParser
|
source_suffix = {
|
||||||
|
'.rst': 'restructuredtext',
|
||||||
source_parsers = {
|
'.txt': 'markdown',
|
||||||
'.md': CommonMarkParser,
|
'.md': 'markdown',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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.
|
# The encoding of source files.
|
||||||
#
|
#
|
||||||
# source_encoding = 'utf-8-sig'
|
# source_encoding = 'utf-8-sig'
|
||||||
|
@ -60,28 +56,26 @@ source_suffix = ['.rst', '.md']
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Docker SDK for Python'
|
project = 'Docker SDK for Python'
|
||||||
year = datetime.datetime.now().year
|
year = datetime.datetime.now().year
|
||||||
copyright = u'%d Docker Inc' % year
|
copyright = f'{year} Docker Inc'
|
||||||
author = u'Docker Inc'
|
author = 'Docker Inc'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
with open('../docker/version.py', 'r') as vfile:
|
# see https://github.com/pypa/setuptools_scm#usage-from-sphinx
|
||||||
exec(vfile.read())
|
release = version('docker')
|
||||||
# The full version, including alpha/beta/rc tags.
|
# for example take major/minor
|
||||||
release = version
|
version = '.'.join(release.split('.')[:2])
|
||||||
# The short X.Y version.
|
|
||||||
version = '{}.{}'.format(version_info[0], version_info[1])
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
language = 'en'
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
# non-false value, then it is used:
|
# non-false value, then it is used:
|
||||||
|
@ -283,8 +277,8 @@ latex_elements = {
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'docker-sdk-python.tex', u'docker-sdk-python Documentation',
|
(master_doc, 'docker-sdk-python.tex', 'docker-sdk-python Documentation',
|
||||||
u'Docker Inc.', 'manual'),
|
'Docker Inc.', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
@ -325,7 +319,7 @@ latex_documents = [
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
(master_doc, 'docker-sdk-python', u'docker-sdk-python Documentation',
|
(master_doc, 'docker-sdk-python', 'docker-sdk-python Documentation',
|
||||||
[author], 1)
|
[author], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -340,7 +334,7 @@ man_pages = [
|
||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'docker-sdk-python', u'docker-sdk-python Documentation',
|
(master_doc, 'docker-sdk-python', 'docker-sdk-python Documentation',
|
||||||
author, 'docker-sdk-python', 'One line description of project.',
|
author, 'docker-sdk-python', 'One line description of project.',
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,7 +15,7 @@ For example, to check the server against a specific CA certificate:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem')
|
tls_config = docker.tls.TLSConfig(ca_cert='/path/to/ca.pem', verify=True)
|
||||||
client = docker.DockerClient(base_url='<https_url>', tls=tls_config)
|
client = docker.DockerClient(base_url='<https_url>', tls=tls_config)
|
||||||
|
|
||||||
This is the equivalent of ``docker --tlsverify --tlscacert /path/to/ca.pem ...``.
|
This is the equivalent of ``docker --tlsverify --tlscacert /path/to/ca.pem ...``.
|
||||||
|
|
|
@ -16,10 +16,13 @@ Prepare the command we are going to use. It prints "hello stdout"
|
||||||
in `stdout`, followed by "hello stderr" in `stderr`:
|
in `stdout`, followed by "hello stderr" in `stderr`:
|
||||||
|
|
||||||
>>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"'
|
>>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"'
|
||||||
|
|
||||||
We'll run this command with all four the combinations of ``stream``
|
We'll run this command with all four the combinations of ``stream``
|
||||||
and ``demux``.
|
and ``demux``.
|
||||||
|
|
||||||
With ``stream=False`` and ``demux=False``, the output is a string
|
With ``stream=False`` and ``demux=False``, the output is a string
|
||||||
that contains both the `stdout` and the `stderr` output:
|
that contains both the `stdout` and the `stderr` output:
|
||||||
|
|
||||||
>>> res = container.exec_run(cmd, stream=False, demux=False)
|
>>> res = container.exec_run(cmd, stream=False, demux=False)
|
||||||
>>> res.output
|
>>> res.output
|
||||||
b'hello stderr\nhello stdout\n'
|
b'hello stderr\nhello stdout\n'
|
||||||
|
@ -52,15 +55,8 @@ Traceback (most recent call last):
|
||||||
File "<stdin>", line 1, in <module>
|
File "<stdin>", line 1, in <module>
|
||||||
StopIteration
|
StopIteration
|
||||||
|
|
||||||
Finally, with ``stream=False`` and ``demux=True``, the whole output
|
Finally, with ``stream=False`` and ``demux=True``, the output is a tuple ``(stdout, stderr)``:
|
||||||
is returned, but the streams are still separated:
|
|
||||||
|
|
||||||
>>> res = container.exec_run(cmd, stream=True, demux=True)
|
>>> res = container.exec_run(cmd, stream=False, demux=True)
|
||||||
>>> next(res.output)
|
>>> res.output
|
||||||
(b'hello stdout\n', None)
|
(b'hello stdout\n', b'hello stderr\n')
|
||||||
>>> next(res.output)
|
|
||||||
(None, b'hello stderr\n')
|
|
||||||
>>> next(res.output)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "<stdin>", line 1, in <module>
|
|
||||||
StopIteration
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
[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"]
|
|
@ -1,17 +0,0 @@
|
||||||
appdirs==1.4.3
|
|
||||||
asn1crypto==0.22.0
|
|
||||||
backports.ssl-match-hostname==3.5.0.1
|
|
||||||
cffi==1.14.4
|
|
||||||
cryptography==3.4.7
|
|
||||||
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
|
|
||||||
urllib3==1.24.3
|
|
||||||
websocket-client==0.56.0
|
|
|
@ -52,8 +52,8 @@ class Version(namedtuple('_Version', 'major minor patch stage edition')):
|
||||||
return (int(self.major), int(self.minor), int(self.patch)) + stage
|
return (int(self.major), int(self.minor), int(self.patch)) + stage
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
stage = '-{}'.format(self.stage) if self.stage else ''
|
stage = f'-{self.stage}' if self.stage else ''
|
||||||
edition = '-{}'.format(self.edition) if self.edition else ''
|
edition = f'-{self.edition}' if self.edition else ''
|
||||||
return '.'.join(map(str, self[:3])) + edition + stage
|
return '.'.join(map(str, self[:3])) + edition + stage
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
description_file = README.rst
|
|
||||||
license = Apache License 2.0
|
|
85
setup.py
85
setup.py
|
@ -1,85 +0,0 @@
|
||||||
#!/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 = [
|
|
||||||
'websocket-client >= 0.32.0',
|
|
||||||
'requests >= 2.14.2, != 2.18.0',
|
|
||||||
]
|
|
||||||
|
|
||||||
extras_require = {
|
|
||||||
# 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>=3.4.7', '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='>=3.6',
|
|
||||||
zip_safe=False,
|
|
||||||
test_suite='tests',
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 5 - Production/Stable',
|
|
||||||
'Environment :: Other Environment',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.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='Ulysses Souza',
|
|
||||||
maintainer_email='ulysses.souza@docker.com',
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
setuptools==54.1.1
|
|
||||||
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
|
|
|
@ -1,17 +1,16 @@
|
||||||
ARG PYTHON_VERSION=3.7
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
ARG PYTHON_VERSION=3.12
|
||||||
FROM python:${PYTHON_VERSION}
|
FROM python:${PYTHON_VERSION}
|
||||||
|
|
||||||
ARG APT_MIRROR
|
|
||||||
RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \
|
|
||||||
&& sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install --no-install-recommends \
|
RUN apt-get update && apt-get -y install --no-install-recommends \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Add SSH keys and set permissions
|
# Add SSH keys and set permissions
|
||||||
COPY tests/ssh-keys /root/.ssh
|
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
|
||||||
RUN chmod -R 600 /root/.ssh
|
RUN chmod -R 600 /root/.ssh
|
||||||
|
|
||||||
COPY ./tests/gpg-keys /gpg-keys
|
COPY ./tests/gpg-keys /gpg-keys
|
||||||
|
@ -27,11 +26,10 @@ RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \
|
||||||
chmod +x /usr/local/bin/docker-credential-pass
|
chmod +x /usr/local/bin/docker-credential-pass
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY requirements.txt /src/requirements.txt
|
COPY . .
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY test-requirements.txt /src/test-requirements.txt
|
ARG VERSION=0.0.0.dev0
|
||||||
RUN pip install -r test-requirements.txt
|
RUN --mount=type=cache,target=/cache/pip \
|
||||||
|
PIP_CACHE_DIR=/cache/pip \
|
||||||
COPY . /src
|
SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \
|
||||||
RUN pip install .
|
pip install .[dev,ssh,websockets]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
ARG PYTHON_VERSION=3.6
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
ARG PYTHON_VERSION=3.12
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}
|
FROM python:${PYTHON_VERSION}
|
||||||
RUN mkdir /tmp/certs
|
RUN mkdir /tmp/certs
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
ARG API_VERSION=1.39
|
# syntax=docker/dockerfile:1
|
||||||
ARG ENGINE_VERSION=19.03.12
|
|
||||||
|
ARG API_VERSION=1.45
|
||||||
|
ARG ENGINE_VERSION=26.1
|
||||||
|
|
||||||
FROM docker:${ENGINE_VERSION}-dind
|
FROM docker:${ENGINE_VERSION}-dind
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache --upgrade \
|
||||||
openssh
|
openssh
|
||||||
|
|
||||||
# Add the keys and set permissions
|
COPY tests/ssh/config/server /etc/ssh/
|
||||||
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
|
# set authorized keys for client paswordless connection
|
||||||
COPY tests/ssh-keys/authorized_keys /root/.ssh/authorized_keys
|
COPY tests/ssh/config/client/id_rsa.pub /root/.ssh/authorized_keys
|
||||||
RUN chmod 600 /root/.ssh/authorized_keys
|
|
||||||
|
|
||||||
RUN echo "root:root" | chpasswd
|
# RUN echo "root:root" | chpasswd
|
||||||
RUN ln -s /usr/local/bin/docker /usr/bin/docker
|
RUN chmod -R 600 /etc/ssh \
|
||||||
|
&& chmod -R 600 /root/.ssh \
|
||||||
|
&& ln -s /usr/local/bin/docker /usr/bin/docker
|
||||||
EXPOSE 22
|
EXPOSE 22
|
||||||
|
|
|
@ -8,10 +8,10 @@ import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import docker
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import pytest
|
import pytest
|
||||||
import six
|
|
||||||
|
import docker
|
||||||
|
|
||||||
|
|
||||||
def make_tree(dirs, files):
|
def make_tree(dirs, files):
|
||||||
|
@ -47,6 +47,19 @@ def untar_file(tardata, filename):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def skip_if_desktop():
|
||||||
|
def fn(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapped(self, *args, **kwargs):
|
||||||
|
info = self.client.info()
|
||||||
|
if info['Name'] == 'docker-desktop':
|
||||||
|
pytest.skip('Test does not support Docker Desktop')
|
||||||
|
return f(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
return fn
|
||||||
|
|
||||||
def requires_api_version(version):
|
def requires_api_version(version):
|
||||||
test_version = os.environ.get(
|
test_version = os.environ.get(
|
||||||
'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION
|
'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION
|
||||||
|
@ -54,7 +67,7 @@ def requires_api_version(version):
|
||||||
|
|
||||||
return pytest.mark.skipif(
|
return pytest.mark.skipif(
|
||||||
docker.utils.version_lt(test_version, version),
|
docker.utils.version_lt(test_version, version),
|
||||||
reason="API version is too low (< {0})".format(version)
|
reason=f"API version is too low (< {version})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,12 +94,12 @@ def wait_on_condition(condition, delay=0.1, timeout=40):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while not condition():
|
while not condition():
|
||||||
if time.time() - start_time > timeout:
|
if time.time() - start_time > timeout:
|
||||||
raise AssertionError("Timeout: %s" % condition)
|
raise AssertionError(f"Timeout: {condition}")
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
|
|
||||||
def random_name():
|
def random_name():
|
||||||
return u'dockerpytest_{0:x}'.format(random.getrandbits(64))
|
return f'dockerpytest_{random.getrandbits(64):x}'
|
||||||
|
|
||||||
|
|
||||||
def force_leave_swarm(client):
|
def force_leave_swarm(client):
|
||||||
|
@ -105,11 +118,11 @@ def force_leave_swarm(client):
|
||||||
|
|
||||||
|
|
||||||
def swarm_listen_addr():
|
def swarm_listen_addr():
|
||||||
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000))
|
return f'0.0.0.0:{random.randrange(10000, 25000)}'
|
||||||
|
|
||||||
|
|
||||||
def assert_cat_socket_detached_with_keys(sock, inputs):
|
def assert_cat_socket_detached_with_keys(sock, inputs):
|
||||||
if six.PY3 and hasattr(sock, '_sock'):
|
if hasattr(sock, '_sock'):
|
||||||
sock = sock._sock
|
sock = sock._sock
|
||||||
|
|
||||||
for i in inputs:
|
for i in inputs:
|
||||||
|
@ -128,7 +141,7 @@ def assert_cat_socket_detached_with_keys(sock, inputs):
|
||||||
# of the daemon no longer cause this to raise an error.
|
# of the daemon no longer cause this to raise an error.
|
||||||
try:
|
try:
|
||||||
sock.sendall(b'make sure the socket is closed\n')
|
sock.sendall(b'make sure the socket is closed\n')
|
||||||
except socket.error:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|
||||||
sock.sendall(b"make sure the socket is closed\n")
|
sock.sendall(b"make sure the socket is closed\n")
|
||||||
|
@ -144,4 +157,4 @@ def ctrl_with(char):
|
||||||
if re.match('[a-z]', char):
|
if re.match('[a-z]', char):
|
||||||
return chr(ord(char) - ord('a') + 1).encode('ascii')
|
return chr(ord(char) - ord('a') + 1).encode('ascii')
|
||||||
else:
|
else:
|
||||||
raise(Exception('char must be [a-z]'))
|
raise Exception('char must be [a-z]')
|
||||||
|
|
|
@ -3,14 +3,13 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from docker import errors
|
from docker import errors
|
||||||
from docker.utils.proxy import ProxyConfig
|
from docker.utils.proxy import ProxyConfig
|
||||||
|
|
||||||
import pytest
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .base import BaseAPIIntegrationTest, TEST_IMG
|
|
||||||
from ..helpers import random_name, requires_api_version, requires_experimental
|
from ..helpers import random_name, requires_api_version, requires_experimental
|
||||||
|
from .base import TEST_IMG, BaseAPIIntegrationTest
|
||||||
|
|
||||||
|
|
||||||
class BuildTest(BaseAPIIntegrationTest):
|
class BuildTest(BaseAPIIntegrationTest):
|
||||||
|
@ -71,9 +70,8 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
assert len(logs) > 0
|
assert len(logs) > 0
|
||||||
|
|
||||||
def test_build_from_stringio(self):
|
def test_build_from_stringio(self):
|
||||||
if six.PY3:
|
return
|
||||||
return
|
script = io.StringIO('\n'.join([
|
||||||
script = io.StringIO(six.text_type('\n').join([
|
|
||||||
'FROM busybox',
|
'FROM busybox',
|
||||||
'RUN mkdir -p /tmp/test',
|
'RUN mkdir -p /tmp/test',
|
||||||
'EXPOSE 8080',
|
'EXPOSE 8080',
|
||||||
|
@ -83,8 +81,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
stream = self.client.build(fileobj=script)
|
stream = self.client.build(fileobj=script)
|
||||||
logs = ''
|
logs = ''
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
if six.PY3:
|
chunk = chunk.decode('utf-8')
|
||||||
chunk = chunk.decode('utf-8')
|
|
||||||
logs += chunk
|
logs += chunk
|
||||||
assert logs != ''
|
assert logs != ''
|
||||||
|
|
||||||
|
@ -103,7 +100,9 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
'ignored',
|
'ignored',
|
||||||
'Dockerfile',
|
'Dockerfile',
|
||||||
'.dockerignore',
|
'.dockerignore',
|
||||||
|
' ignored-with-spaces ', # check that spaces are trimmed
|
||||||
'!ignored/subdir/excepted-file',
|
'!ignored/subdir/excepted-file',
|
||||||
|
'! ignored/subdir/excepted-with-spaces '
|
||||||
'', # empty line,
|
'', # empty line,
|
||||||
'#*', # comment line
|
'#*', # comment line
|
||||||
]))
|
]))
|
||||||
|
@ -114,6 +113,9 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
with open(os.path.join(base_dir, '#file.txt'), 'w') as f:
|
with open(os.path.join(base_dir, '#file.txt'), 'w') as f:
|
||||||
f.write('this file should not be ignored')
|
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')
|
subdir = os.path.join(base_dir, 'ignored', 'subdir')
|
||||||
os.makedirs(subdir)
|
os.makedirs(subdir)
|
||||||
with open(os.path.join(subdir, 'file'), 'w') as f:
|
with open(os.path.join(subdir, 'file'), 'w') as f:
|
||||||
|
@ -122,12 +124,15 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
with open(os.path.join(subdir, 'excepted-file'), 'w') as f:
|
with open(os.path.join(subdir, 'excepted-file'), 'w') as f:
|
||||||
f.write("this file should not be ignored")
|
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'
|
tag = 'docker-py-test-build-with-dockerignore'
|
||||||
stream = self.client.build(
|
stream = self.client.build(
|
||||||
path=base_dir,
|
path=base_dir,
|
||||||
tag=tag,
|
tag=tag,
|
||||||
)
|
)
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
c = self.client.create_container(tag, ['find', '/test', '-type', 'f'])
|
c = self.client.create_container(tag, ['find', '/test', '-type', 'f'])
|
||||||
|
@ -135,11 +140,11 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
self.client.wait(c)
|
self.client.wait(c)
|
||||||
logs = self.client.logs(c)
|
logs = self.client.logs(c)
|
||||||
|
|
||||||
if six.PY3:
|
logs = logs.decode('utf-8')
|
||||||
logs = logs.decode('utf-8')
|
|
||||||
|
|
||||||
assert sorted(list(filter(None, logs.split('\n')))) == sorted([
|
assert sorted(filter(None, logs.split('\n'))) == sorted([
|
||||||
'/test/#file.txt',
|
'/test/#file.txt',
|
||||||
|
'/test/ignored/subdir/excepted-with-spaces',
|
||||||
'/test/ignored/subdir/excepted-file',
|
'/test/ignored/subdir/excepted-file',
|
||||||
'/test/not-ignored'
|
'/test/not-ignored'
|
||||||
])
|
])
|
||||||
|
@ -155,7 +160,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
fileobj=script, tag='buildargs', buildargs={'test': 'OK'}
|
fileobj=script, tag='buildargs', buildargs={'test': 'OK'}
|
||||||
)
|
)
|
||||||
self.tmp_imgs.append('buildargs')
|
self.tmp_imgs.append('buildargs')
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
info = self.client.inspect_image('buildargs')
|
info = self.client.inspect_image('buildargs')
|
||||||
|
@ -175,7 +180,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
fileobj=script, tag=tag, shmsize=shmsize
|
fileobj=script, tag=tag, shmsize=shmsize
|
||||||
)
|
)
|
||||||
self.tmp_imgs.append(tag)
|
self.tmp_imgs.append(tag)
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# There is currently no way to get the shmsize
|
# There is currently no way to get the shmsize
|
||||||
|
@ -193,7 +198,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
isolation='default'
|
isolation='default'
|
||||||
)
|
)
|
||||||
|
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@requires_api_version('1.23')
|
@requires_api_version('1.23')
|
||||||
|
@ -208,7 +213,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
fileobj=script, tag='labels', labels=labels
|
fileobj=script, tag='labels', labels=labels
|
||||||
)
|
)
|
||||||
self.tmp_imgs.append('labels')
|
self.tmp_imgs.append('labels')
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
info = self.client.inspect_image('labels')
|
info = self.client.inspect_image('labels')
|
||||||
|
@ -225,7 +230,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
stream = self.client.build(fileobj=script, tag='build1')
|
stream = self.client.build(fileobj=script, tag='build1')
|
||||||
self.tmp_imgs.append('build1')
|
self.tmp_imgs.append('build1')
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
stream = self.client.build(
|
stream = self.client.build(
|
||||||
|
@ -266,11 +271,11 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
fileobj=script, target='first', tag='build1'
|
fileobj=script, target='first', tag='build1'
|
||||||
)
|
)
|
||||||
self.tmp_imgs.append('build1')
|
self.tmp_imgs.append('build1')
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
info = self.client.inspect_image('build1')
|
info = self.client.inspect_image('build1')
|
||||||
assert not info['Config']['OnBuild']
|
assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild']
|
||||||
|
|
||||||
@requires_api_version('1.25')
|
@requires_api_version('1.25')
|
||||||
def test_build_with_network_mode(self):
|
def test_build_with_network_mode(self):
|
||||||
|
@ -295,7 +300,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tmp_imgs.append('dockerpytest_customnetbuild')
|
self.tmp_imgs.append('dockerpytest_customnetbuild')
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert self.client.inspect_image('dockerpytest_customnetbuild')
|
assert self.client.inspect_image('dockerpytest_customnetbuild')
|
||||||
|
@ -307,7 +312,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tmp_imgs.append('dockerpytest_nonebuild')
|
self.tmp_imgs.append('dockerpytest_nonebuild')
|
||||||
logs = [chunk for chunk in stream]
|
logs = list(stream)
|
||||||
assert 'errorDetail' in logs[-1]
|
assert 'errorDetail' in logs[-1]
|
||||||
assert logs[-1]['errorDetail']['code'] == 1
|
assert logs[-1]['errorDetail']['code'] == 1
|
||||||
|
|
||||||
|
@ -340,8 +345,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
assert self.client.inspect_image(img_name)
|
assert self.client.inspect_image(img_name)
|
||||||
ctnr = self.run_container(img_name, 'cat /hosts-file')
|
ctnr = self.run_container(img_name, 'cat /hosts-file')
|
||||||
logs = self.client.logs(ctnr)
|
logs = self.client.logs(ctnr)
|
||||||
if six.PY3:
|
logs = logs.decode('utf-8')
|
||||||
logs = logs.decode('utf-8')
|
|
||||||
assert '127.0.0.1\textrahost.local.test' in logs
|
assert '127.0.0.1\textrahost.local.test' in logs
|
||||||
assert '127.0.0.1\thello.world.test' in logs
|
assert '127.0.0.1\thello.world.test' in logs
|
||||||
|
|
||||||
|
@ -361,7 +365,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
fileobj=script, tag=tag, squash=squash
|
fileobj=script, tag=tag, squash=squash
|
||||||
)
|
)
|
||||||
self.tmp_imgs.append(tag)
|
self.tmp_imgs.append(tag)
|
||||||
for chunk in stream:
|
for _chunk in stream:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.client.inspect_image(tag)
|
return self.client.inspect_image(tag)
|
||||||
|
@ -376,7 +380,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)'
|
snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)'
|
||||||
script = io.BytesIO(b'\n'.join([
|
script = io.BytesIO(b'\n'.join([
|
||||||
b'FROM busybox',
|
b'FROM busybox',
|
||||||
'RUN sh -c ">&2 echo \'{0}\'"'.format(snippet).encode('utf-8')
|
f'RUN sh -c ">&2 echo \'{snippet}\'"'.encode('utf-8')
|
||||||
]))
|
]))
|
||||||
|
|
||||||
stream = self.client.build(
|
stream = self.client.build(
|
||||||
|
@ -385,10 +389,8 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
lines = []
|
lines = []
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
lines.append(chunk.get('stream'))
|
lines.append(chunk.get('stream'))
|
||||||
expected = '{0}{2}\n{1}'.format(
|
expected = f'{control_chars[0]}{snippet}\n{control_chars[1]}'
|
||||||
control_chars[0], control_chars[1], snippet
|
assert any(line == expected for line in lines)
|
||||||
)
|
|
||||||
assert any([line == expected for line in lines])
|
|
||||||
|
|
||||||
def test_build_gzip_encoding(self):
|
def test_build_gzip_encoding(self):
|
||||||
base_dir = tempfile.mkdtemp()
|
base_dir = tempfile.mkdtemp()
|
||||||
|
@ -440,7 +442,7 @@ class BuildTest(BaseAPIIntegrationTest):
|
||||||
@requires_api_version('1.32')
|
@requires_api_version('1.32')
|
||||||
@requires_experimental(until=None)
|
@requires_experimental(until=None)
|
||||||
def test_build_invalid_platform(self):
|
def test_build_invalid_platform(self):
|
||||||
script = io.BytesIO('FROM busybox\n'.encode('ascii'))
|
script = io.BytesIO(b'FROM busybox\n')
|
||||||
|
|
||||||
with pytest.raises(errors.APIError) as excinfo:
|
with pytest.raises(errors.APIError) as excinfo:
|
||||||
stream = self.client.build(fileobj=script, platform='foobar')
|
stream = self.client.build(fileobj=script, platform='foobar')
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ConnectionTimeoutTest(unittest.TestCase):
|
||||||
# This call isn't supposed to complete, and it should fail fast.
|
# This call isn't supposed to complete, and it should fail fast.
|
||||||
try:
|
try:
|
||||||
res = self.client.inspect_container('id')
|
res = self.client.inspect_container('id')
|
||||||
except: # noqa: E722
|
except Exception:
|
||||||
pass
|
pass
|
||||||
end = time.time()
|
end = time.time()
|
||||||
assert res is None
|
assert res is None
|
||||||
|
@ -72,6 +72,4 @@ class UnixconnTest(unittest.TestCase):
|
||||||
client.close()
|
client.close()
|
||||||
del client
|
del client
|
||||||
|
|
||||||
assert len(w) == 0, "No warnings produced: {0}".format(
|
assert len(w) == 0, f"No warnings produced: {w[0].message}"
|
||||||
w[0].message
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
import pytest
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..helpers import force_leave_swarm, requires_api_version
|
from ..helpers import force_leave_swarm, requires_api_version
|
||||||
from .base import BaseAPIIntegrationTest
|
from .base import BaseAPIIntegrationTest
|
||||||
|
@ -31,7 +30,7 @@ class ConfigAPITest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
def test_create_config_unicode_data(self):
|
def test_create_config_unicode_data(self):
|
||||||
config_id = self.client.create_config(
|
config_id = self.client.create_config(
|
||||||
'favorite_character', u'いざよいさくや'
|
'favorite_character', 'いざよいさくや'
|
||||||
)
|
)
|
||||||
self.tmp_configs.append(config_id)
|
self.tmp_configs.append(config_id)
|
||||||
assert 'ID' in config_id
|
assert 'ID' in config_id
|
||||||
|
@ -70,3 +69,16 @@ class ConfigAPITest(BaseAPIIntegrationTest):
|
||||||
data = self.client.configs(filters={'name': ['favorite_character']})
|
data = self.client.configs(filters={'name': ['favorite_character']})
|
||||||
assert len(data) == 1
|
assert len(data) == 1
|
||||||
assert data[0]['ID'] == config_id['ID']
|
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'
|
||||||
|
|
|
@ -9,15 +9,17 @@ import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
from .. import helpers
|
|
||||||
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.constants import IS_WINDOWS_PLATFORM
|
||||||
from docker.utils.socket import next_frame_header
|
from docker.utils.socket import next_frame_header, read_exactly
|
||||||
from docker.utils.socket import 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
|
||||||
|
|
||||||
|
|
||||||
class ListContainersTest(BaseAPIIntegrationTest):
|
class ListContainersTest(BaseAPIIntegrationTest):
|
||||||
|
@ -34,7 +36,7 @@ class ListContainersTest(BaseAPIIntegrationTest):
|
||||||
assert len(retrieved) == 1
|
assert len(retrieved) == 1
|
||||||
retrieved = retrieved[0]
|
retrieved = retrieved[0]
|
||||||
assert 'Command' in retrieved
|
assert 'Command' in retrieved
|
||||||
assert retrieved['Command'] == str('true')
|
assert retrieved['Command'] == 'true'
|
||||||
assert 'Image' in retrieved
|
assert 'Image' in retrieved
|
||||||
assert re.search(r'alpine:.*', retrieved['Image'])
|
assert re.search(r'alpine:.*', retrieved['Image'])
|
||||||
assert 'Status' in retrieved
|
assert 'Status' in retrieved
|
||||||
|
@ -104,10 +106,10 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
assert self.client.wait(container3_id)['StatusCode'] == 0
|
assert self.client.wait(container3_id)['StatusCode'] == 0
|
||||||
|
|
||||||
logs = self.client.logs(container3_id).decode('utf-8')
|
logs = self.client.logs(container3_id).decode('utf-8')
|
||||||
assert '{0}_NAME='.format(link_env_prefix1) in logs
|
assert f'{link_env_prefix1}_NAME=' in logs
|
||||||
assert '{0}_ENV_FOO=1'.format(link_env_prefix1) in logs
|
assert f'{link_env_prefix1}_ENV_FOO=1' in logs
|
||||||
assert '{0}_NAME='.format(link_env_prefix2) in logs
|
assert f'{link_env_prefix2}_NAME=' in logs
|
||||||
assert '{0}_ENV_FOO=1'.format(link_env_prefix2) in logs
|
assert f'{link_env_prefix2}_ENV_FOO=1' in logs
|
||||||
|
|
||||||
def test_create_with_restart_policy(self):
|
def test_create_with_restart_policy(self):
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
|
@ -122,8 +124,8 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
self.client.wait(id)
|
self.client.wait(id)
|
||||||
with pytest.raises(docker.errors.APIError) as exc:
|
with pytest.raises(docker.errors.APIError) as exc:
|
||||||
self.client.remove_container(id)
|
self.client.remove_container(id)
|
||||||
err = exc.value.explanation
|
err = exc.value.explanation.lower()
|
||||||
assert 'You cannot remove ' in err
|
assert 'stop the container before' in err
|
||||||
self.client.remove_container(id, force=True)
|
self.client.remove_container(id, force=True)
|
||||||
|
|
||||||
def test_create_container_with_volumes_from(self):
|
def test_create_container_with_volumes_from(self):
|
||||||
|
@ -215,6 +217,20 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
self.client.kill(id)
|
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):
|
def test_group_id_ints(self):
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'id -G',
|
TEST_IMG, 'id -G',
|
||||||
|
@ -460,16 +476,13 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
def test_create_with_device_cgroup_rules(self):
|
def test_create_with_device_cgroup_rules(self):
|
||||||
rule = 'c 7:128 rwm'
|
rule = 'c 7:128 rwm'
|
||||||
ctnr = self.client.create_container(
|
ctnr = self.client.create_container(
|
||||||
TEST_IMG, 'cat /sys/fs/cgroup/devices/devices.list',
|
TEST_IMG, 'true', host_config=self.client.create_host_config(
|
||||||
host_config=self.client.create_host_config(
|
|
||||||
device_cgroup_rules=[rule]
|
device_cgroup_rules=[rule]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.tmp_containers.append(ctnr)
|
self.tmp_containers.append(ctnr)
|
||||||
config = self.client.inspect_container(ctnr)
|
config = self.client.inspect_container(ctnr)
|
||||||
assert config['HostConfig']['DeviceCgroupRules'] == [rule]
|
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):
|
def test_create_with_uts_mode(self):
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
|
@ -487,7 +500,7 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
)
|
)
|
||||||
class VolumeBindTest(BaseAPIIntegrationTest):
|
class VolumeBindTest(BaseAPIIntegrationTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(VolumeBindTest, self).setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.mount_dest = '/mnt'
|
self.mount_dest = '/mnt'
|
||||||
|
|
||||||
|
@ -531,6 +544,27 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
||||||
inspect_data = self.client.inspect_container(container)
|
inspect_data = self.client.inspect_container(container)
|
||||||
self.check_container_data(inspect_data, False)
|
self.check_container_data(inspect_data, False)
|
||||||
|
|
||||||
|
@skip_if_desktop()
|
||||||
|
def test_create_with_binds_rw_rshared(self):
|
||||||
|
container = self.run_with_volume_propagation(
|
||||||
|
False,
|
||||||
|
'rshared',
|
||||||
|
TEST_IMG,
|
||||||
|
['touch', os.path.join(self.mount_dest, self.filename)],
|
||||||
|
)
|
||||||
|
inspect_data = self.client.inspect_container(container)
|
||||||
|
self.check_container_data(inspect_data, True, 'rshared')
|
||||||
|
container = self.run_with_volume_propagation(
|
||||||
|
True,
|
||||||
|
'rshared',
|
||||||
|
TEST_IMG,
|
||||||
|
['ls', self.mount_dest],
|
||||||
|
)
|
||||||
|
logs = self.client.logs(container).decode('utf-8')
|
||||||
|
assert self.filename in logs
|
||||||
|
inspect_data = self.client.inspect_container(container)
|
||||||
|
self.check_container_data(inspect_data, False, 'rshared')
|
||||||
|
|
||||||
@requires_api_version('1.30')
|
@requires_api_version('1.30')
|
||||||
def test_create_with_mounts(self):
|
def test_create_with_mounts(self):
|
||||||
mount = docker.types.Mount(
|
mount = docker.types.Mount(
|
||||||
|
@ -586,7 +620,57 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
||||||
assert mount['Source'] == mount_data['Name']
|
assert mount['Source'] == mount_data['Name']
|
||||||
assert mount_data['RW'] is True
|
assert mount_data['RW'] is True
|
||||||
|
|
||||||
def check_container_data(self, inspect_data, rw):
|
@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'):
|
||||||
assert 'Mounts' in inspect_data
|
assert 'Mounts' in inspect_data
|
||||||
filtered = list(filter(
|
filtered = list(filter(
|
||||||
lambda x: x['Destination'] == self.mount_dest,
|
lambda x: x['Destination'] == self.mount_dest,
|
||||||
|
@ -596,6 +680,7 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
||||||
mount_data = filtered[0]
|
mount_data = filtered[0]
|
||||||
assert mount_data['Source'] == self.mount_origin
|
assert mount_data['Source'] == self.mount_origin
|
||||||
assert mount_data['RW'] == rw
|
assert mount_data['RW'] == rw
|
||||||
|
assert mount_data['Propagation'] == propagation
|
||||||
|
|
||||||
def run_with_volume(self, ro, *args, **kwargs):
|
def run_with_volume(self, ro, *args, **kwargs):
|
||||||
return self.run_container(
|
return self.run_container(
|
||||||
|
@ -613,12 +698,29 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def run_with_volume_propagation(self, ro, propagation, *args, **kwargs):
|
||||||
|
return self.run_container(
|
||||||
|
*args,
|
||||||
|
volumes={self.mount_dest: {}},
|
||||||
|
host_config=self.client.create_host_config(
|
||||||
|
binds={
|
||||||
|
self.mount_origin: {
|
||||||
|
'bind': self.mount_dest,
|
||||||
|
'ro': ro,
|
||||||
|
'propagation': propagation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
network_mode='none'
|
||||||
|
),
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArchiveTest(BaseAPIIntegrationTest):
|
class ArchiveTest(BaseAPIIntegrationTest):
|
||||||
def test_get_file_archive_from_container(self):
|
def test_get_file_archive_from_container(self):
|
||||||
data = 'The Maid and the Pocket Watch of Blood'
|
data = 'The Maid and the Pocket Watch of Blood'
|
||||||
ctnr = self.client.create_container(
|
ctnr = self.client.create_container(
|
||||||
TEST_IMG, 'sh -c "echo {0} > /vol1/data.txt"'.format(data),
|
TEST_IMG, f'sh -c "echo {data} > /vol1/data.txt"',
|
||||||
volumes=['/vol1']
|
volumes=['/vol1']
|
||||||
)
|
)
|
||||||
self.tmp_containers.append(ctnr)
|
self.tmp_containers.append(ctnr)
|
||||||
|
@ -636,7 +738,7 @@ class ArchiveTest(BaseAPIIntegrationTest):
|
||||||
def test_get_file_stat_from_container(self):
|
def test_get_file_stat_from_container(self):
|
||||||
data = 'The Maid and the Pocket Watch of Blood'
|
data = 'The Maid and the Pocket Watch of Blood'
|
||||||
ctnr = self.client.create_container(
|
ctnr = self.client.create_container(
|
||||||
TEST_IMG, 'sh -c "echo -n {0} > /vol1/data.txt"'.format(data),
|
TEST_IMG, f'sh -c "echo -n {data} > /vol1/data.txt"',
|
||||||
volumes=['/vol1']
|
volumes=['/vol1']
|
||||||
)
|
)
|
||||||
self.tmp_containers.append(ctnr)
|
self.tmp_containers.append(ctnr)
|
||||||
|
@ -655,9 +757,7 @@ class ArchiveTest(BaseAPIIntegrationTest):
|
||||||
test_file.seek(0)
|
test_file.seek(0)
|
||||||
ctnr = self.client.create_container(
|
ctnr = self.client.create_container(
|
||||||
TEST_IMG,
|
TEST_IMG,
|
||||||
'cat {0}'.format(
|
f"cat {os.path.join('/vol1/', os.path.basename(test_file.name))}",
|
||||||
os.path.join('/vol1/', os.path.basename(test_file.name))
|
|
||||||
),
|
|
||||||
volumes=['/vol1']
|
volumes=['/vol1']
|
||||||
)
|
)
|
||||||
self.tmp_containers.append(ctnr)
|
self.tmp_containers.append(ctnr)
|
||||||
|
@ -701,7 +801,7 @@ class RenameContainerTest(BaseAPIIntegrationTest):
|
||||||
if version == '1.5.0':
|
if version == '1.5.0':
|
||||||
assert name == inspect['Name']
|
assert name == inspect['Name']
|
||||||
else:
|
else:
|
||||||
assert '/{0}'.format(name) == inspect['Name']
|
assert f'/{name}' == inspect['Name']
|
||||||
|
|
||||||
|
|
||||||
class StartContainerTest(BaseAPIIntegrationTest):
|
class StartContainerTest(BaseAPIIntegrationTest):
|
||||||
|
@ -807,7 +907,7 @@ class LogsTest(BaseAPIIntegrationTest):
|
||||||
def test_logs(self):
|
def test_logs(self):
|
||||||
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo {0}'.format(snippet)
|
TEST_IMG, f'echo {snippet}'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -815,13 +915,13 @@ class LogsTest(BaseAPIIntegrationTest):
|
||||||
exitcode = self.client.wait(id)['StatusCode']
|
exitcode = self.client.wait(id)['StatusCode']
|
||||||
assert exitcode == 0
|
assert exitcode == 0
|
||||||
logs = self.client.logs(id)
|
logs = self.client.logs(id)
|
||||||
assert logs == (snippet + '\n').encode(encoding='ascii')
|
assert logs == f"{snippet}\n".encode(encoding='ascii')
|
||||||
|
|
||||||
def test_logs_tail_option(self):
|
def test_logs_tail_option(self):
|
||||||
snippet = '''Line1
|
snippet = '''Line1
|
||||||
Line2'''
|
Line2'''
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo "{0}"'.format(snippet)
|
TEST_IMG, f'echo "{snippet}"'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -834,7 +934,7 @@ Line2'''
|
||||||
def test_logs_streaming_and_follow(self):
|
def test_logs_streaming_and_follow(self):
|
||||||
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo {0}'.format(snippet)
|
TEST_IMG, f'echo {snippet}'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -846,7 +946,7 @@ Line2'''
|
||||||
exitcode = self.client.wait(id)['StatusCode']
|
exitcode = self.client.wait(id)['StatusCode']
|
||||||
assert exitcode == 0
|
assert exitcode == 0
|
||||||
|
|
||||||
assert logs == (snippet + '\n').encode(encoding='ascii')
|
assert logs == f"{snippet}\n".encode(encoding='ascii')
|
||||||
|
|
||||||
@pytest.mark.timeout(5)
|
@pytest.mark.timeout(5)
|
||||||
@pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'),
|
@pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'),
|
||||||
|
@ -854,7 +954,7 @@ Line2'''
|
||||||
def test_logs_streaming_and_follow_and_cancel(self):
|
def test_logs_streaming_and_follow_and_cancel(self):
|
||||||
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'sh -c "echo \\"{0}\\" && sleep 3"'.format(snippet)
|
TEST_IMG, f'sh -c "echo \\"{snippet}\\" && sleep 3"'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -867,12 +967,12 @@ Line2'''
|
||||||
for chunk in generator:
|
for chunk in generator:
|
||||||
logs += chunk
|
logs += chunk
|
||||||
|
|
||||||
assert logs == (snippet + '\n').encode(encoding='ascii')
|
assert logs == f"{snippet}\n".encode(encoding='ascii')
|
||||||
|
|
||||||
def test_logs_with_dict_instead_of_id(self):
|
def test_logs_with_dict_instead_of_id(self):
|
||||||
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo {0}'.format(snippet)
|
TEST_IMG, f'echo {snippet}'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -880,12 +980,12 @@ Line2'''
|
||||||
exitcode = self.client.wait(id)['StatusCode']
|
exitcode = self.client.wait(id)['StatusCode']
|
||||||
assert exitcode == 0
|
assert exitcode == 0
|
||||||
logs = self.client.logs(container)
|
logs = self.client.logs(container)
|
||||||
assert logs == (snippet + '\n').encode(encoding='ascii')
|
assert logs == f"{snippet}\n".encode(encoding='ascii')
|
||||||
|
|
||||||
def test_logs_with_tail_0(self):
|
def test_logs_with_tail_0(self):
|
||||||
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
snippet = 'Flowering Nights (Sakuya Iyazoi)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo "{0}"'.format(snippet)
|
TEST_IMG, f'echo "{snippet}"'
|
||||||
)
|
)
|
||||||
id = container['Id']
|
id = container['Id']
|
||||||
self.tmp_containers.append(id)
|
self.tmp_containers.append(id)
|
||||||
|
@ -899,7 +999,7 @@ Line2'''
|
||||||
def test_logs_with_until(self):
|
def test_logs_with_until(self):
|
||||||
snippet = 'Shanghai Teahouse (Hong Meiling)'
|
snippet = 'Shanghai Teahouse (Hong Meiling)'
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo "{0}"'.format(snippet)
|
TEST_IMG, f'echo "{snippet}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tmp_containers.append(container)
|
self.tmp_containers.append(container)
|
||||||
|
@ -909,7 +1009,7 @@ Line2'''
|
||||||
logs_until_1 = self.client.logs(container, until=1)
|
logs_until_1 = self.client.logs(container, until=1)
|
||||||
assert logs_until_1 == b''
|
assert logs_until_1 == b''
|
||||||
logs_until_now = self.client.logs(container, datetime.now())
|
logs_until_now = self.client.logs(container, datetime.now())
|
||||||
assert logs_until_now == (snippet + '\n').encode(encoding='ascii')
|
assert logs_until_now == f"{snippet}\n".encode(encoding='ascii')
|
||||||
|
|
||||||
|
|
||||||
class DiffTest(BaseAPIIntegrationTest):
|
class DiffTest(BaseAPIIntegrationTest):
|
||||||
|
@ -1075,7 +1175,7 @@ class PortTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
ip, host_port = port_binding['HostIp'], port_binding['HostPort']
|
ip, host_port = port_binding['HostIp'], port_binding['HostPort']
|
||||||
|
|
||||||
port_binding = port if not protocol else port + "/" + protocol
|
port_binding = port if not protocol else f"{port}/{protocol}"
|
||||||
assert ip == port_bindings[port_binding][0]
|
assert ip == port_bindings[port_binding][0]
|
||||||
assert host_port == port_bindings[port_binding][1]
|
assert host_port == port_bindings[port_binding][1]
|
||||||
|
|
||||||
|
@ -1095,7 +1195,7 @@ class ContainerTopTest(BaseAPIIntegrationTest):
|
||||||
self.client.start(container)
|
self.client.start(container)
|
||||||
res = self.client.top(container)
|
res = self.client.top(container)
|
||||||
if not IS_WINDOWS_PLATFORM:
|
if not IS_WINDOWS_PLATFORM:
|
||||||
assert res['Titles'] == [u'PID', u'USER', u'TIME', u'COMMAND']
|
assert res['Titles'] == ['PID', 'USER', 'TIME', 'COMMAND']
|
||||||
assert len(res['Processes']) == 1
|
assert len(res['Processes']) == 1
|
||||||
assert res['Processes'][0][-1] == 'sleep 60'
|
assert res['Processes'][0][-1] == 'sleep 60'
|
||||||
self.client.kill(container)
|
self.client.kill(container)
|
||||||
|
@ -1113,7 +1213,7 @@ class ContainerTopTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
self.client.start(container)
|
self.client.start(container)
|
||||||
res = self.client.top(container, '-eopid,user')
|
res = self.client.top(container, '-eopid,user')
|
||||||
assert res['Titles'] == [u'PID', u'USER']
|
assert res['Titles'] == ['PID', 'USER']
|
||||||
assert len(res['Processes']) == 1
|
assert len(res['Processes']) == 1
|
||||||
assert res['Processes'][0][10] == 'sleep 60'
|
assert res['Processes'][0][10] == 'sleep 60'
|
||||||
|
|
||||||
|
@ -1200,10 +1300,10 @@ class AttachContainerTest(BaseAPIIntegrationTest):
|
||||||
sock = self.client.attach_socket(container, ws=False)
|
sock = self.client.attach_socket(container, ws=False)
|
||||||
assert sock.fileno() > -1
|
assert sock.fileno() > -1
|
||||||
|
|
||||||
def test_run_container_reading_socket(self):
|
def test_run_container_reading_socket_http(self):
|
||||||
line = 'hi there and stuff and things, words!'
|
line = 'hi there and stuff and things, words!'
|
||||||
# `echo` appends CRLF, `printf` doesn't
|
# `echo` appends CRLF, `printf` doesn't
|
||||||
command = "printf '{0}'".format(line)
|
command = f"printf '{line}'"
|
||||||
container = self.client.create_container(TEST_IMG, command,
|
container = self.client.create_container(TEST_IMG, command,
|
||||||
detach=True, tty=False)
|
detach=True, tty=False)
|
||||||
self.tmp_containers.append(container)
|
self.tmp_containers.append(container)
|
||||||
|
@ -1220,12 +1320,33 @@ class AttachContainerTest(BaseAPIIntegrationTest):
|
||||||
data = read_exactly(pty_stdout, next_size)
|
data = read_exactly(pty_stdout, next_size)
|
||||||
assert data.decode('utf-8') == line
|
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):
|
def test_attach_no_stream(self):
|
||||||
container = self.client.create_container(
|
container = self.client.create_container(
|
||||||
TEST_IMG, 'echo hello'
|
TEST_IMG, 'echo hello'
|
||||||
)
|
)
|
||||||
self.tmp_containers.append(container)
|
self.tmp_containers.append(container)
|
||||||
self.client.start(container)
|
self.client.start(container)
|
||||||
|
self.client.wait(container, condition='not-running')
|
||||||
output = self.client.attach(container, stream=False, logs=True)
|
output = self.client.attach(container, stream=False, logs=True)
|
||||||
assert output == 'hello\n'.encode(encoding='ascii')
|
assert output == 'hello\n'.encode(encoding='ascii')
|
||||||
|
|
||||||
|
@ -1360,7 +1481,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
|
||||||
response = self.client.stats(container, stream=0)
|
response = self.client.stats(container, stream=0)
|
||||||
self.client.kill(container)
|
self.client.kill(container)
|
||||||
|
|
||||||
assert type(response) == dict
|
assert isinstance(response, dict)
|
||||||
for key in ['read', 'networks', 'precpu_stats', 'cpu_stats',
|
for key in ['read', 'networks', 'precpu_stats', 'cpu_stats',
|
||||||
'memory_stats', 'blkio_stats']:
|
'memory_stats', 'blkio_stats']:
|
||||||
assert key in response
|
assert key in response
|
||||||
|
@ -1373,7 +1494,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest):
|
||||||
self.client.start(container)
|
self.client.start(container)
|
||||||
stream = self.client.stats(container)
|
stream = self.client.stats(container)
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
assert type(chunk) == dict
|
assert isinstance(chunk, dict)
|
||||||
for key in ['read', 'network', 'precpu_stats', 'cpu_stats',
|
for key in ['read', 'network', 'precpu_stats', 'cpu_stats',
|
||||||
'memory_stats', 'blkio_stats']:
|
'memory_stats', 'blkio_stats']:
|
||||||
assert key in chunk
|
assert key in chunk
|
||||||
|
@ -1487,7 +1608,7 @@ class LinkTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
# Remove link
|
# Remove link
|
||||||
linked_name = self.client.inspect_container(container2_id)['Name'][1:]
|
linked_name = self.client.inspect_container(container2_id)['Name'][1:]
|
||||||
link_name = '%s/%s' % (linked_name, link_alias)
|
link_name = f'{linked_name}/{link_alias}'
|
||||||
self.client.remove_container(link_name, link=True)
|
self.client.remove_container(link_name, link=True)
|
||||||
|
|
||||||
# Link is gone
|
# Link is gone
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from ..helpers import assert_cat_socket_detached_with_keys
|
|
||||||
from ..helpers import ctrl_with
|
|
||||||
from ..helpers import requires_api_version
|
|
||||||
from .base import BaseAPIIntegrationTest
|
|
||||||
from .base import TEST_IMG
|
|
||||||
from docker.utils.proxy import ProxyConfig
|
from docker.utils.proxy import ProxyConfig
|
||||||
from docker.utils.socket import next_frame_header
|
from docker.utils.socket import next_frame_header, read_exactly
|
||||||
from docker.utils.socket import read_exactly
|
|
||||||
|
from ..helpers import (
|
||||||
|
assert_cat_socket_detached_with_keys,
|
||||||
|
ctrl_with,
|
||||||
|
requires_api_version,
|
||||||
|
)
|
||||||
|
from .base import TEST_IMG, BaseAPIIntegrationTest
|
||||||
|
|
||||||
|
|
||||||
class ExecTest(BaseAPIIntegrationTest):
|
class ExecTest(BaseAPIIntegrationTest):
|
||||||
|
@ -239,7 +240,7 @@ class ExecDemuxTest(BaseAPIIntegrationTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ExecDemuxTest, self).setUp()
|
super().setUp()
|
||||||
self.container = self.client.create_container(
|
self.container = self.client.create_container(
|
||||||
TEST_IMG, 'cat', detach=True, stdin_open=True
|
TEST_IMG, 'cat', detach=True, stdin_open=True
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue