Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api refactoring #208

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ docs/_build
.mypy_cache
.python-version
.pytest_cache
.idea
1 change: 1 addition & 0 deletions CHANGES/203.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new low-level API and high-level model classes for Images and Containers
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Andreas Krebs
Andrew Svetlov
Byeongjun Park
Cecil Tonglet
Expand Down
6 changes: 5 additions & 1 deletion aiodocker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from .docker import Docker
from .api.client import APIClient
from .client import DockerClient


__version__ = '0.11.0a0'


__all__ = ("Docker", )
__all__ = ("Docker",
"APIClient",
"DockerClient")
1 change: 1 addition & 0 deletions aiodocker/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .client import APIClient
226 changes: 226 additions & 0 deletions aiodocker/api/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import asyncio
import json
import logging
import os
from pathlib import Path
import re
import ssl

import aiohttp
from yarl import URL

from ..utils import utils

# Sub-API classes
from .container import DockerContainerAPI
from ..events import DockerEvents
from ..errors import create_api_error_from_response
from .image import DockerImageAPI
from .system import DockerSystemAPI
# from .logs import DockerLog
# from .swarm import DockerSwarm
# from .services import DockerServices
# from .tasks import DockerTasks
# from .volumes import DockerVolumes, DockerVolume
# from .nodes import DockerSwarmNodes
# from .system import DockerSystem

# __all__ = (
# 'Docker',
# 'DockerContainers', 'DockerContainer',
# 'DockerEvents',
# 'DockerError',
# 'DockerImages',
# 'DockerLog',
# 'DockerSwarm',
# 'DockerServices',
# 'DockerTasks',
# 'DockerVolumes', 'DockerVolume',
# 'DockerSwarmNodes',
# 'DockerSystem'
# )

log = logging.getLogger(__name__)

_sock_search_paths = [
Path('/run/docker.sock'),
Path('/var/run/docker.sock'),
]

_rx_version = re.compile(r'^v\d+\.\d+$')
_rx_tcp_schemes = re.compile(r'^(tcp|http)://')


class APIClient:
def __init__(self,
url=None,
connector=None,
session=None,
ssl_context=None,
api_version='v1.30'):

docker_host = url # rename
if docker_host is None:
docker_host = os.environ.get('DOCKER_HOST', None)
if docker_host is None:
for sockpath in _sock_search_paths:
if sockpath.is_socket():
docker_host = 'unix://' + str(sockpath)
break
self.docker_host = docker_host

assert _rx_version.search(api_version) is not None, \
'Invalid API version format'
self.api_version = api_version

if docker_host is None:
raise ValueError(
"Missing valid docker_host."
"Either DOCKER_HOST or local sockets are not available."
)

if connector is None:
if _rx_tcp_schemes.search(docker_host):
if os.environ.get('DOCKER_TLS_VERIFY', '0') == '1':
ssl_context = self._docker_machine_ssl_context()
docker_host = _rx_tcp_schemes.sub('https://', docker_host)
else:
ssl_context = None
connector = aiohttp.TCPConnector(ssl_context=ssl_context)
self.docker_host = docker_host
elif docker_host.startswith('unix://'):
connector = aiohttp.UnixConnector(docker_host[7:])
# dummy hostname for URL composition
self.docker_host = "unix://localhost"
else:
raise ValueError('Missing protocol scheme in docker_host.')
self.connector = connector
if session is None:
session = aiohttp.ClientSession(connector=self.connector)
self.session = session

self.events = DockerEvents(self)
self._container = DockerContainerAPI(self)
self._image = DockerImageAPI(self)
# self.swarm = DockerSwarm(self)
# self.services = DockerServices(self)
# self.tasks = DockerTasks(self)
# self.volumes = DockerVolumes(self)
# self.nodes = DockerSwarmNodes(self)
self._system = DockerSystemAPI(self)

async def close(self):
await self.events.stop()
await self.session.close()

async def auth(self, **credentials):
response = await self._query_json(
"auth", "POST",
data=credentials,
)
return response

async def version(self):
data = await self._query_json("version")
return data

def _canonicalize_url(self, path):
return URL("{self.docker_host}/{self.api_version}/{path}"
.format(self=self, path=path))

async def _query(self, path, method='GET', *,
params=None, data=None, headers=None,
timeout=None):
'''
Get the response object by performing the HTTP request.
The caller is responsible to finalize the response object.
'''
url = self._canonicalize_url(path)
if headers and 'content-type' not in headers:
headers['content-type'] = 'application/json'
try:
response = await self.session.request(
method, url,
params=utils.httpize(params),
headers=headers,
data=data,
timeout=timeout)
except asyncio.TimeoutError:
raise
if (response.status // 100) in [4, 5]:
await create_api_error_from_response(response)
return response

async def _query_json(self, path, method='GET', *,
params=None, data=None, headers=None,
timeout=None):
"""
A shorthand of _query() that treats the input as JSON.
"""
if headers is None:
headers = {}
headers['content-type'] = 'application/json'
if not isinstance(data, (str, bytes)):
data = json.dumps(data)
response = await self._query(
path, method,
params=params, data=data, headers=headers,
timeout=timeout)
data = await utils.parse_result(response)
return data

async def _websocket(self, path, **params):
if not params:
params = {
'stdin': True,
'stdout': True,
'stderr': True,
'stream': True
}
url = self._canonicalize_url(path)
# ws_connect() does not have params arg.
url = url.with_query(utils.httpize(params))
ws = await self.session.ws_connect(
url,
protocols=['chat'],
origin='http://localhost',
autoping=True,
autoclose=True)
return ws

@staticmethod
def _docker_machine_ssl_context():
"""
Create a SSLContext object using DOCKER_* env vars.
"""
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
certs_path = os.environ.get('DOCKER_CERT_PATH', None)
if certs_path is None:
raise ValueError("Cannot create ssl context, "
"DOCKER_CERT_PATH is not set!")
certs_path = Path(certs_path)
context.load_verify_locations(cafile=certs_path / 'ca.pem')
context.load_cert_chain(certfile=certs_path / 'cert.pem',
keyfile=certs_path / 'key.pem')
return context

@property
def container(self):
"""
An object for managing containers on the server. See the
:ref:`low-level containers documentation <low-level-containers>` for full details.
"""
return self._container

@property
def image(self):
"""
An object for managing images on the server. See the
:ref:`low-level images documentation <low-level-images>` for full details.
"""
return self._image

@property
def system(self):
return self._system
Loading