-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathdockerclient.py
More file actions
201 lines (155 loc) · 7.68 KB
/
dockerclient.py
File metadata and controls
201 lines (155 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# -*- coding: utf-8 -*-
"""Support the Deis workflow by manipulating and publishing Docker images."""
import logging
import os
import backoff
from django.conf import settings
from rest_framework.exceptions import PermissionDenied
from simpleflock import SimpleFlock
import docker
import docker.constants
from docker.auth import auth
from docker.errors import APIError
logger = logging.getLogger(__name__)
class RegistryException(Exception):
pass
class DockerClient(object):
"""Use the Docker API to pull, tag, build, and push images to deis-registry."""
FLOCKFILE = '/tmp/controller-pull'
def __init__(self):
timeout = os.environ.get('DOCKER_CLIENT_TIMEOUT', docker.constants.DEFAULT_TIMEOUT_SECONDS)
self.client = docker.Client(version='auto', timeout=timeout)
self.registry = settings.REGISTRY_HOST + ':' + str(settings.REGISTRY_PORT)
def login(self, repository, creds=None):
"""Log into a registry if auth is provided"""
if not creds:
return
# parse out the hostname since repo variable is hostname + path
registry, _ = auth.resolve_repository_name(repository)
registry_auth = {
'username': None,
'password': None,
'email': None,
'registry': registry
}
registry_auth.update(creds)
if not registry_auth['username'] or not registry_auth['password']:
msg = 'Registry auth requires a username and a password'
logger.error(msg)
raise PermissionDenied(msg)
logger.info('Logging into Registry {} with username {}'.format(repository, registry_auth['username'])) # noqa
response = self.client.login(**registry_auth)
success = response.get('Status') == 'Login Succeeded' or response.get('username') == registry_auth['username'] # noqa
if not success:
raise PermissionDenied('Could not log into {} with username {}'.format(repository, registry_auth['username'])) # noqa
logger.info('Successfully logged into {} with {}'.format(repository, registry_auth['username'])) # noqa
def get_port(self, target, deis_registry=False, creds=None):
"""
Get a port from a Docker image
"""
# get the target repository name and tag
name, _ = docker.utils.parse_repository_tag(target)
# strip any "http://host.domain:port" prefix from the target repository name,
# since we always publish to the Deis registry
repo, name = auth.split_repo_name(name)
# log into pull repo
if not deis_registry:
self.login(repo, creds)
info = self.inspect_image(target, deis_registry)
if 'ExposedPorts' not in info['Config']:
return None
port = int(list(info['Config']['ExposedPorts'].keys())[0].split('/')[0])
return port
def publish_release(self, source, target, deis_registry=False, creds=None):
"""Update a source Docker image with environment config and publish it to deis-registry."""
# get the source repository name and tag
src_name, src_tag = docker.utils.parse_repository_tag(source)
# get the target repository name and tag
name, tag = docker.utils.parse_repository_tag(target)
# strip any "http://host.domain:port" prefix from the target repository name,
# since we always publish to the Deis registry
repo, name = auth.split_repo_name(name)
# pull the source image from the registry
# NOTE: this relies on an implementation detail of deis-builder, that
# the image has been uploaded already to deis-registry
if deis_registry:
repo = "{}/{}".format(self.registry, src_name)
else:
repo = src_name
try:
# log into pull repo
if not deis_registry:
self.login(repo, creds)
# pull image from source repository
self.pull(repo, src_tag, deis_registry)
# tag the image locally without the repository URL
image = "{}:{}".format(src_name, src_tag)
self.tag(image, "{}/{}".format(self.registry, name), tag=tag)
# push the image to deis-registry
self.push("{}/{}".format(self.registry, name), tag)
except APIError as e:
raise RegistryException(str(e))
def pull(self, repo, tag, insecure_registry=True):
"""Pull a Docker image into the local storage graph."""
check_blacklist(repo)
logger.info("Pulling Docker image {}:{}".format(repo, tag))
with SimpleFlock(self.FLOCKFILE, timeout=1200):
stream = self.client.pull(
repo, tag=tag, stream=True, decode=True,
insecure_registry=insecure_registry
)
log_output(stream, 'pull', repo, tag)
def push(self, repo, tag):
"""Push a local Docker image to a registry."""
logger.info("Pushing Docker image {}:{}".format(repo, tag))
stream = self.client.push(
repo, tag=tag, stream=True, decode=True,
insecure_registry=True
)
log_output(stream, 'push', repo, tag)
def tag(self, image, repo, tag):
"""Tag a local Docker image with a new name and tag."""
check_blacklist(repo)
logger.info("Tagging Docker image {} as {}:{}".format(image, repo, tag))
if not self.client.tag(image, repo, tag=tag, force=True):
raise RegistryException('Tagging {} as {}:{} failed'.format(image, repo, tag))
@backoff.on_exception(backoff.expo, Exception, max_tries=3)
def inspect_image(self, target, insecure_registry=True):
"""
Inspect docker image to gather information from it
try thrice to find the port before raising exception as docker-py is flaky
"""
# image already includes the tag, so we split it out here
repo, tag = docker.utils.parse_repository_tag(target)
# make sure image is pulled locally already
self.pull(repo, tag=tag, insecure_registry=insecure_registry)
# inspect the image
return self.client.inspect_image(target)
def check_blacklist(repo):
"""Check a Docker repository name for collision with deis/* components."""
blacklisted = [ # NOTE: keep this list up to date!
'builder', 'controller', 'database', 'dockerbuilder', 'etcd', 'minio', 'registry',
'router', 'slugbuilder', 'slugrunner', 'workflow', 'workflow-manager',
]
if any("deis/{}".format(c) in repo for c in blacklisted):
raise PermissionDenied("Repository name {} is not allowed, as it is reserved by Deis".format(repo)) # noqa
def log_output(stream, operation, repo, tag):
"""Log a stream at DEBUG level, and raise RegistryException if it contains an error"""
for chunk in stream:
# error handling requires looking at the response body
if 'error' in chunk:
stream_error(chunk, operation, repo, tag)
def stream_error(chunk, operation, repo, tag):
"""Translate docker stream errors into a more digestable format"""
# grab the generic error and strip the useless Error: portion
message = chunk['error'].replace('Error: ', '')
# not all errors provide the code
if 'code' in chunk['errorDetail']:
# permission denied on the repo
if chunk['errorDetail']['code'] == 403:
message = 'Permission Denied attempting to {} image {}:{}'.format(operation, repo, tag)
raise RegistryException(message)
def publish_release(source, target, deis_registry, creds=None):
return DockerClient().publish_release(source, target, deis_registry, creds)
def get_port(target, deis_registry, creds=None):
return DockerClient().get_port(target, deis_registry, creds)