1
0
mirror of https://github.com/Tecnativa/docker-socket-proxy synced 2024-10-13 03:18:57 +00:00

Add tests and CI (#34)

* Add first version of tests

From https://github.com/Tecnativa/docker-socket-proxy/pull/14

* Expand tests

* Add GH CI

* Apply suggestions

* Apply autopretty template + fix prettier

* Fix isort

* Apply autoprettier

* Fix VSCode settings

* Make tests run in parallel

* Build docker image before testing

* Update workspace settings

* Try multi-platform builds and push to ghcr.io

* Push to docker hub as well from ci

* Upgrade autopretty

* Update pyproject configurations

* Improve test configuration and execution

TT26468

* Provide initial conftest

* Improve tests

* Add python3 in image

* Remove POST rule from proxy

* Build image before testing and push at the end

Builds the image (in single arch) before testing
Loads the image into local docker (See https://github.com/docker/build-push-action#export-image-to-docker)
Rebuilds and pushes the final image in multi-arch at the end.

* Fix python path

* Remove build fixture from tests to see if image is built in CI

* Organize docker tests definition and document

* Restore fixture allowing usage for local testing

This reverts commit dc0b60e63f and allows using `--prebuild` CLI flag for pytest when doing local tests.

Co-authored-by: Jairo Llopis <jairo.llopis@tecnativa.com>
This commit is contained in:
João Marques 2020-12-10 08:52:55 +00:00 committed by GitHub
parent a07d4ae4d9
commit e84babd1c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1254 additions and 69 deletions

View File

@ -0,0 +1,10 @@
# Changes here will be overwritten by Copier; do NOT edit manually
_commit: v0.1.0a5
_src_path: https://github.com/copier-org/autopretty.git
ansible: false
biggest_kbs: 0
github: true
js: false
protected_branches:
- master
python: true

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
# For isort
profile = black
[*.{code-snippets,code-workspace,json,yaml,yml}{,.jinja}]
indent_size = 2

4
.flake8 Normal file
View File

@ -0,0 +1,4 @@
[flake8]
ignore = E203, E501, W503, B950
max-line-length = 88
select = C,E,F,W,B

15
.github/workflows/pre-commit.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: pre-commit
on:
pull_request:
push:
branches:
- master
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.0

103
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,103 @@
name: test
on:
pull_request:
push:
branches:
- master
workflow_dispatch:
inputs:
pytest_addopts:
description:
Extra options for pytest; use -vv for full details; see
https://docs.pytest.org/en/latest/example/simple.html#how-to-change-command-line-options-defaults
required: false
env:
LANG: "en_US.utf-8"
LC_ALL: "en_US.utf-8"
PIP_CACHE_DIR: ${{ github.workspace }}/.cache.~/pip
PIPX_HOME: ${{ github.workspace }}/.cache.~/pipx
POETRY_CACHE_DIR: ${{ github.workspace }}/.cache.~/pypoetry
POETRY_VIRTUALENVS_IN_PROJECT: "true"
PYTEST_ADDOPTS: ${{ github.event.inputs.pytest_addopts }}
PYTHONIOENCODING: "UTF-8"
jobs:
build-test-push:
runs-on: ubuntu-latest
env:
DOCKER_REPO: tecnativa/docker-socket-proxy
steps:
# Prepare Docker environment and build
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build image(s)
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
# HACK: Build single platform image for testing. See https://github.com/docker/buildx/issues/59
load: true
push: false
tags: |
${{ env.DOCKER_REPO }}:local
# Set up and run tests
- name: Install python
uses: actions/setup-python@v1
with:
python-version: "3.9"
- name: Generate cache key CACHE
run:
echo "CACHE=${{ secrets.CACHE_DATE }} ${{ runner.os }} $(python -VV |
sha256sum | cut -d' ' -f1) ${{ hashFiles('pyproject.toml') }} ${{
hashFiles('poetry.lock') }}" >> $GITHUB_ENV
- uses: actions/cache@v2
with:
path: |
.cache.~
.venv
~/.local/bin
key: venv ${{ env.CACHE }}
- run: pip install poetry
- name: Patch $PATH
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
- run: poetry install
# Run tests
- run: poetry run pytest
env:
DOCKER_IMAGE_NAME: ${{ env.DOCKER_REPO }}:local
# Build and push
- name: Login to DockerHub
if:
github.repository == 'Tecnativa/docker-socket-proxy' && github.ref ==
'refs/heads/master'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_LOGIN }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if:
github.repository == 'Tecnativa/docker-socket-proxy' && github.ref ==
'refs/heads/master'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ secrets.BOT_LOGIN }}
password: ${{ secrets.BOT_TOKEN }}
- name: Build and push
if:
github.repository == 'Tecnativa/docker-socket-proxy' && github.ref ==
'refs/heads/master'
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm/v8,linux/arm64,linux/ppc64le,linux/s390x
load: false
push: true
tags: |
ghcr.io/${{ env.DOCKER_REPO }}
${{ env.DOCKER_REPO }}

260
.gitignore vendored Normal file
View File

@ -0,0 +1,260 @@
# Created by https://www.toptal.com/developers/gitignore/api/vscode,python,node
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,python,node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### vscode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# End of https://www.toptal.com/developers/gitignore/api/vscode,python,node

89
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,89 @@
default_language_version:
python: python3
node: "14.14.0"
repos:
# General
- repo: local
hooks:
- id: forbidden-files
name: forbidden files
entry: found forbidden files; remove them
language: fail
files: "\\.rej$"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-xml
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
- id: mixed-line-ending
args:
- --fix=lf
- id: no-commit-to-branch
args:
- --branch=master
- id: trailing-whitespace
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: debug-statements
- id: fix-encoding-pragma
args:
- --remove
- id: requirements-txt-fixer
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.1.2
hooks:
- id: prettier
additional_dependencies:
- prettier@2.1.2
- "@prettier/plugin-xml@0.12.0"
args:
- --plugin=@prettier/plugin-xml
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
args:
- --in-place
- --expand-star-imports
- --ignore-init-module-imports
- --remove-all-unused-imports
- --remove-duplicate-keys
- --remove-unused-variables
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.2
hooks:
- id: pyupgrade
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
- repo: https://github.com/timothycrosley/isort
rev: 5.5.1
hooks:
- id: isort
args:
- --settings=.
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3
hooks:
- &flake8
id: flake8
name: flake8 except __init__.py
exclude: /__init__\.py$
additional_dependencies:
- flake8-bugbear==20.1.4
- <<: *flake8
name: flake8 for __init__.py
args:
# ignore unused imports in __init__.py
- --extend-ignore=F401
files: /__init__\.py$

3
.prettierrc.yml Normal file
View File

@ -0,0 +1,3 @@
printWidth: 88
proseWrap: always
xmlWhitespaceSensitivity: "ignore"

14
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"cSpell.words": ["pytest"],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"python.formatting.provider": "black",
"python.linting.flake8Enabled": true,
"python.pythonPath": ".venv/bin/python",
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "ms-python.python",
"editor.formatOnSave": true
}
}

View File

@ -29,6 +29,11 @@ ENV ALLOW_RESTARTS=0 \
VOLUMES=0
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
# Install python/pip
ENV PYTHONUNBUFFERED=1
RUN apk add --update --no-cache python3 && ln -sf $(which python3) /usr/local/bin/python
RUN python -m ensurepip && python -m pip install --no-cache --upgrade pip setuptools
# Metadata
ARG VCS_REF
ARG BUILD_DATE

175
README.md
View File

@ -11,36 +11,36 @@ This is a security-enhanced proxy for the Docker Socket.
## Why?
Giving access to your Docker socket could mean giving root access to your host,
or even to your whole swarm, but some services require hooking into that socket
to react to events, etc. Using this proxy lets you block anything you consider
those services should not do.
Giving access to your Docker socket could mean giving root access to your host, or even
to your whole swarm, but some services require hooking into that socket to react to
events, etc. Using this proxy lets you block anything you consider those services should
not do.
## How?
We use the official [Alpine][]-based [HAProxy][] image with a small
configuration file.
We use the official [Alpine][]-based [HAProxy][] image with a small configuration file.
It blocks access to the Docker socket API according to the environment
variables you set. It returns a `HTTP 403 Forbidden` status for those dangerous
requests that should never happen.
It blocks access to the Docker socket API according to the environment variables you
set. It returns a `HTTP 403 Forbidden` status for those dangerous requests that should
never happen.
## Security recommendations
- Never expose this container's port to a public network. Only to a Docker
networks where only reside the proxy itself and the service that uses it.
- Revoke access to any API section that you consider your service should not
need.
- This image does not include TLS support, just plain HTTP proxy to the host
Docker Unix socket (which is not TLS protected even if you configured your
host for TLS protection). This is by design because you are supposed to
restrict access to it through Docker's built-in firewall.
- [Read the docs](#suppported-api-versions) for the API version you are using,
and **know what you are doing**.
- Never expose this container's port to a public network. Only to a Docker networks
where only reside the proxy itself and the service that uses it.
- Revoke access to any API section that you consider your service should not need.
- This image does not include TLS support, just plain HTTP proxy to the host Docker
Unix socket (which is not TLS protected even if you configured your host for TLS
protection). This is by design because you are supposed to restrict access to it
through Docker's built-in firewall.
- [Read the docs](#suppported-api-versions) for the API version you are using, and
**know what you are doing**.
## Usage
1. Run the API proxy (`--privileged` flag is required here because it connects with the docker socket, which is a privileged connection in some SELinux/AppArmor contexts and would get locked otherwise):
1. Run the API proxy (`--privileged` flag is required here because it connects with the
docker socket, which is a privileged connection in some SELinux/AppArmor contexts
and would get locked otherwise):
$ docker container run \
-d --privileged \
@ -80,85 +80,124 @@ requests that should never happen.
Request forbidden by administrative rules.
</body></html>
The same will happen to any containers that use this proxy's `2375` port to
access the Docker socket API.
The same will happen to any containers that use this proxy's `2375` port to access the
Docker socket API.
## Grant or revoke access to certain API sections
You grant and revoke access to certain features of the Docker API through
environment variables.
You grant and revoke access to certain features of the Docker API through environment
variables.
Normally the variables match the URL prefix (i.e. `AUTH` blocks access to
`/auth/*` parts of the API, etc.).
Normally the variables match the URL prefix (i.e. `AUTH` blocks access to `/auth/*`
parts of the API, etc.).
Possible values for these variables:
- `0` to **revoke** access.
- `1` to **grant** access.
- `0` to **revoke** access.
- `1` to **grant** access.
### Access granted by default
These API sections are mostly harmless and almost required for any service that
uses the API, so they are granted by default.
These API sections are mostly harmless and almost required for any service that uses the
API, so they are granted by default.
- `EVENTS`
- `PING`
- `VERSION`
- `EVENTS`
- `PING`
- `VERSION`
### Access revoked by default
#### Security-critical
These API sections are considered security-critical, and thus access is revoked
by default. Maximum caution when enabling these.
These API sections are considered security-critical, and thus access is revoked by
default. Maximum caution when enabling these.
- `AUTH`
- `SECRETS`
- `POST`: When disabled, only `GET` and `HEAD` operations are allowed, meaning
any section of the API is read-only.
- `AUTH`
- `SECRETS`
- `POST`: When disabled, only `GET` and `HEAD` operations are allowed, meaning any
section of the API is read-only.
#### Not always needed
You will possibly need to grant access to some of these API sections, which are
not so extremely critical but can expose some information that your service
does not need.
You will possibly need to grant access to some of these API sections, which are not so
extremely critical but can expose some information that your service does not need.
- `BUILD`
- `COMMIT`
- `CONFIGS`
- `CONTAINERS`
- `DISTRIBUTION`
- `EXEC`
- `IMAGES`
- `INFO`
- `NETWORKS`
- `NODES`
- `PLUGINS`
- `SERVICES`
- `SESSION`
- `SWARM`
- `SYSTEM`
- `TASKS`
- `VOLUMES`
- `BUILD`
- `COMMIT`
- `CONFIGS`
- `CONTAINERS`
- `DISTRIBUTION`
- `EXEC`
- `IMAGES`
- `INFO`
- `NETWORKS`
- `NODES`
- `PLUGINS`
- `SERVICES`
- `SESSION`
- `SWARM`
- `SYSTEM`
- `TASKS`
- `VOLUMES`
## Development
All the dependencies you need to develop this project (apart from Docker itself) are
managed with [poetry](https://python-poetry.org/).
To set up your development environment, run:
```
poetry install
```
### Testing
To run the tests locally, add `--prebuild` to autobuild the image before testing:
```sh
poetry run pytest --prebuild
```
By default, the image that the tests use (and optionally prebuild) is named
`docker-socket-proxy:local`. If you prefer, you can build it separately before testing,
and remove the `--prebuild` flag, to run the tests with that image you built:
```sh
docker image build -t docker-socket-proxy:local .
poetry run pytest
```
If you want to use a different image, export the `DOCKER_IMAGE_NAME` env variable with
the name you want:
```sh
# To build it automatically
env DOCKER_IMAGE_NAME=my_custom_image poetry run pytest --prebuild
# To prebuild it separately
docker image build -t my_custom_image .
env DOCKER_IMAGE_NAME=my_custom_image poetry run pytest
```
## Logging
You can set the logging level or severity level of the messages to be logged with the
environment variable `LOG_LEVEL`. Defaul value is info. Possible values are: debug,
info, notice, warning, err, crit, alert and emerg.
environment variable `LOG_LEVEL`. Defaul value is info. Possible values are: debug,
info, notice, warning, err, crit, alert and emerg.
## Supported API versions
- [1.27](https://docs.docker.com/engine/api/v1.27/)
- [1.28](https://docs.docker.com/engine/api/v1.28/)
- [1.29](https://docs.docker.com/engine/api/v1.29/)
- [1.30](https://docs.docker.com/engine/api/v1.30/)
- [1.37](https://docs.docker.com/engine/api/v1.37/)
- [1.27](https://docs.docker.com/engine/api/v1.27/)
- [1.28](https://docs.docker.com/engine/api/v1.28/)
- [1.29](https://docs.docker.com/engine/api/v1.29/)
- [1.30](https://docs.docker.com/engine/api/v1.30/)
- [1.37](https://docs.docker.com/engine/api/v1.37/)
## Feedback
Please send any feedback (issues, questions) to the [issue tracker][].
[Alpine]: https://alpinelinux.org/
[HAProxy]: http://www.haproxy.org/
[alpine]: https://alpinelinux.org/
[haproxy]: http://www.haproxy.org/
[issue tracker]: https://github.com/Tecnativa/docker-socket-proxy/issues

View File

@ -57,7 +57,6 @@ frontend dockerfrontend
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/post } { env(POST) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool }
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool }

465
poetry.lock generated Normal file
View File

@ -0,0 +1,465 @@
[[package]]
name = "apipkg"
version = "1.5"
description = "apipkg: namespace control and lazy-import mechanism"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "20.3.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
[[package]]
name = "black"
version = "20.8b1"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
appdirs = "*"
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.6,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
typed-ast = ">=1.4.0"
typing-extensions = ">=3.7.4"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "execnet"
version = "1.7.1"
description = "execnet: rapid multi-Python deployment"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
apipkg = ">=1.4"
[package.extras]
testing = ["pre-commit"]
[[package]]
name = "flake8"
version = "3.8.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.6.0a1,<2.7.0"
pyflakes = ">=2.2.0,<2.3.0"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "20.4"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "plumbum"
version = "1.6.9"
description = "Plumbum: shell combinators library"
category = "dev"
optional = false
python-versions = ">=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[[package]]
name = "py"
version = "1.9.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycodestyle"
version = "2.6.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyflakes"
version = "2.2.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pytest"
version = "6.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=17.4.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.8.2"
toml = "*"
[package.extras]
checkqa_mypy = ["mypy (==0.780)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-forked"
version = "1.3.0"
description = "run tests in isolated forked subprocesses"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
py = "*"
pytest = ">=3.10"
[[package]]
name = "pytest-xdist"
version = "2.1.0"
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
execnet = ">=1.1"
pytest = ">=6.0.0"
pytest-forked = "*"
[package.extras]
psutil = ["psutil (>=3.0)"]
testing = ["filelock"]
[[package]]
name = "regex"
version = "2020.11.13"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "six"
version = "1.15.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typed-ast"
version = "1.4.1"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "96dcdc93ce97e947b10a856408bfbb970e7df54f4694f7c4c1634b2d0ed8ea6c"
[metadata.files]
apipkg = [
{file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"},
{file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
]
black = [
{file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"},
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
execnet = [
{file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"},
{file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"},
]
flake8 = [
{file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
{file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
]
iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
plumbum = [
{file = "plumbum-1.6.9-py2.py3-none-any.whl", hash = "sha256:91418dcc66b58ab9d2e3b04b3d1e0d787dc45923154fb8b4a826bd9316dba0d6"},
{file = "plumbum-1.6.9.tar.gz", hash = "sha256:16b9e19d96c80f2e9d051ef5f04927b834a6ac0ce5d2768eb8662b5cd53e43df"},
]
py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
{file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
]
pytest-forked = [
{file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"},
{file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"},
]
pytest-xdist = [
{file = "pytest-xdist-2.1.0.tar.gz", hash = "sha256:82d938f1a24186520e2d9d3a64ef7d9ac7ecdf1a0659e095d18e596b8cbd0672"},
{file = "pytest_xdist-2.1.0-py3-none-any.whl", hash = "sha256:7c629016b3bb006b88ac68e2b31551e7becf173c76b977768848e2bbed594d90"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[tool.poetry]
name = "docker-socket-proxy"
version = "0.0.0"
description = ""
authors = ["Tecnativa"]
[tool.poetry.dependencies]
python = "^3.8"
[tool.poetry.dev-dependencies]
black = {version = "^20.8b1", allow-prereleases = true}
flake8 = "^3.8.4"
plumbum = "^1.6.9"
pytest = "^6.1.2"
pytest-xdist = "^2.1.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
addopts = -n auto -ra

64
tests/conftest.py Normal file
View File

@ -0,0 +1,64 @@
import json
import os
from contextlib import contextmanager
from logging import info
from pathlib import Path
import pytest
from plumbum import local
from plumbum.cmd import docker
DOCKER_IMAGE_NAME = os.environ.get("DOCKER_IMAGE_NAME", "docker-socket-proxy:local")
def pytest_addoption(parser):
"""Allow prebuilding image for local testing."""
parser.addoption("--prebuild", action="store_const", const=True)
@pytest.fixture(autouse=True, scope="session")
def prebuild_docker_image(request):
"""Build local docker image once before starting test suite."""
if request.config.getoption("--prebuild"):
info(f"Building {DOCKER_IMAGE_NAME}...")
docker("build", "-t", DOCKER_IMAGE_NAME, Path(__file__).parent.parent)
@contextmanager
def proxy(**env_vars):
"""A context manager that starts the proxy with the specified env.
While inside the block, `$DOCKER_HOST` will be modified to talk to the proxy
instead of the raw docker socket.
"""
container_id = None
env_list = [f"--env={key}={value}" for key, value in env_vars.items()]
info(f"Starting {DOCKER_IMAGE_NAME} container with: {env_list}")
try:
container_id = docker(
"container",
"run",
"--detach",
"--privileged",
"--publish=2375",
"--volume=/var/run/docker.sock:/var/run/docker.sock",
*env_list,
DOCKER_IMAGE_NAME,
).strip()
container_data = json.loads(
docker("container", "inspect", container_id.strip())
)
socket_port = container_data[0]["NetworkSettings"]["Ports"]["2375/tcp"][0][
"HostPort"
]
with local.env(DOCKER_HOST=f"tcp://localhost:{socket_port}"):
yield container_id
finally:
if container_id:
info(f"Removing {container_id}...")
docker(
"container",
"rm",
"-f",
container_id,
)

78
tests/test_service.py Normal file
View File

@ -0,0 +1,78 @@
import logging
import pytest
from conftest import proxy
from plumbum import ProcessExecutionError
from plumbum.cmd import docker
logger = logging.getLogger()
def _check_permissions(allowed_calls, forbidden_calls):
for args in allowed_calls:
docker(*args)
for args in forbidden_calls:
with pytest.raises(ProcessExecutionError):
docker(*args)
def test_default_permissions():
with proxy() as test_container:
allowed_calls = (("version",),)
forbidden_calls = (
("pull", "alpine"),
("--rm", "alpine", "--name", test_container),
("logs", test_container),
("wait", test_container),
("rm", "-f", test_container),
("restart", test_container),
("network", "ls"),
("config", "ls"),
("service", "ls"),
("stack", "ls"),
("secret", "ls"),
("plugin", "ls"),
("info",),
("system", "info"),
("build", "."),
("swarm", "init"),
)
_check_permissions(allowed_calls, forbidden_calls)
def test_container_permissions():
with proxy(CONTAINERS=1) as test_container:
allowed_calls = [
("logs", test_container),
("inspect", test_container),
]
forbidden_calls = [
("wait", test_container),
("run", "--rm", "alpine"),
("rm", "-f", test_container),
("restart", test_container),
]
_check_permissions(allowed_calls, forbidden_calls)
def test_post_permissions():
with proxy(POST=1) as test_container:
allowed_calls = []
forbidden_calls = [
("rm", "-f", test_container),
("pull", "alpine"),
("run", "--rm", "alpine"),
("network", "create", "foobar"),
]
_check_permissions(allowed_calls, forbidden_calls)
def test_network_post_permissions():
with proxy(POST=1, NETWORKS=1):
allowed_calls = [
("network", "ls"),
("network", "create", "foo"),
("network", "rm", "foo"),
]
forbidden_calls = []
_check_permissions(allowed_calls, forbidden_calls)