|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +"""Support the Deis workflow by manipulating and publishing Docker images.""" |
| 3 | + |
| 4 | +from __future__ import unicode_literals |
| 5 | +import io |
| 6 | +import logging |
| 7 | + |
| 8 | +from django.conf import settings |
| 9 | +from rest_framework.exceptions import PermissionDenied |
| 10 | +from simpleflock import SimpleFlock |
| 11 | +import docker |
| 12 | + |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +class DockerClient(object): |
| 17 | + """Use the Docker API to pull, tag, build, and push images to deis-registry.""" |
| 18 | + |
| 19 | + FLOCKFILE = '/tmp/controller-pull' |
| 20 | + |
| 21 | + def __init__(self): |
| 22 | + self.client = docker.Client(version='auto') |
| 23 | + self.registry = settings.REGISTRY_HOST + ':' + str(settings.REGISTRY_PORT) |
| 24 | + |
| 25 | + def publish_release(self, source, config, target, deis_registry): |
| 26 | + """Update a source Docker image with environment config and publish it to deis-registry.""" |
| 27 | + # get the source repository name and tag |
| 28 | + src_name, src_tag = docker.utils.parse_repository_tag(source) |
| 29 | + # get the target repository name and tag |
| 30 | + name, tag = docker.utils.parse_repository_tag(target) |
| 31 | + # strip any "http://host.domain:port" prefix from the target repository name, |
| 32 | + # since we always publish to the Deis registry |
| 33 | + name = strip_prefix(name) |
| 34 | + |
| 35 | + # pull the source image from the registry |
| 36 | + # NOTE: this relies on an implementation detail of deis-builder, that |
| 37 | + # the image has been uploaded already to deis-registry |
| 38 | + if deis_registry: |
| 39 | + repo = "{}/{}".format(self.registry, src_name) |
| 40 | + else: |
| 41 | + repo = src_name |
| 42 | + self.pull(repo, src_tag) |
| 43 | + |
| 44 | + # tag the image locally without the repository URL |
| 45 | + image = "{}:{}".format(repo, src_tag) |
| 46 | + self.tag(image, src_name, tag=src_tag) |
| 47 | + |
| 48 | + # build a Docker image that adds a "last-mile" layer of environment |
| 49 | + config.update({'DEIS_APP': name, 'DEIS_RELEASE': tag}) |
| 50 | + self.build(source, config, name, tag) |
| 51 | + |
| 52 | + # push the image to deis-registry |
| 53 | + self.push("{}/{}".format(self.registry, name), tag) |
| 54 | + |
| 55 | + def build(self, source, config, repo, tag): |
| 56 | + """Add a "last-mile" layer of environment config to a Docker image for deis-registry.""" |
| 57 | + check_blacklist(repo) |
| 58 | + env = ' '.join("{}='{}'".format( |
| 59 | + k, v.encode('unicode-escape').replace("'", "\\'")) for k, v in config.viewitems()) |
| 60 | + dockerfile = "FROM {}\nENV {}".format(source, env) |
| 61 | + f = io.BytesIO(dockerfile.encode('utf-8')) |
| 62 | + target_repo = "{}/{}:{}".format(self.registry, repo, tag) |
| 63 | + logger.info("Building Docker image {}".format(target_repo)) |
| 64 | + with SimpleFlock(self.FLOCKFILE, timeout=1200): |
| 65 | + stream = self.client.build(fileobj=f, tag=target_repo, stream=True, rm=True) |
| 66 | + log_output(stream) |
| 67 | + |
| 68 | + def pull(self, repo, tag): |
| 69 | + """Pull a Docker image into the local storage graph.""" |
| 70 | + check_blacklist(repo) |
| 71 | + logger.info("Pulling Docker image {}:{}".format(repo, tag)) |
| 72 | + with SimpleFlock(self.FLOCKFILE, timeout=1200): |
| 73 | + stream = self.client.pull(repo, tag=tag, stream=True, insecure_registry=True) |
| 74 | + log_output(stream) |
| 75 | + |
| 76 | + def push(self, repo, tag): |
| 77 | + """Push a local Docker image to a registry.""" |
| 78 | + logger.info("Pushing Docker image {}:{}".format(repo, tag)) |
| 79 | + stream = self.client.push(repo, tag=tag, stream=True, insecure_registry=True) |
| 80 | + log_output(stream) |
| 81 | + |
| 82 | + def tag(self, image, repo, tag): |
| 83 | + """Tag a local Docker image with a new name and tag.""" |
| 84 | + check_blacklist(repo) |
| 85 | + logger.info("Tagging Docker image {} as {}:{}".format(image, repo, tag)) |
| 86 | + if not self.client.tag(image, repo, tag=tag, force=True): |
| 87 | + raise docker.errors.DockerException("tagging failed") |
| 88 | + |
| 89 | + |
| 90 | +def check_blacklist(repo): |
| 91 | + """Check a Docker repository name for collision with deis/* components.""" |
| 92 | + blacklisted = [ # NOTE: keep this list up to date! |
| 93 | + 'builder', 'cache', 'controller', 'database', 'logger', 'logspout', |
| 94 | + 'publisher', 'registry', 'router', 'store-admin', 'store-daemon', |
| 95 | + 'store-gateway', 'store-metadata', 'store-monitor', 'swarm', 'mesos-master', |
| 96 | + 'mesos-marathon', 'mesos-slave', 'zookeeper', |
| 97 | + ] |
| 98 | + if any("deis/{}".format(c) in repo for c in blacklisted): |
| 99 | + raise PermissionDenied("Repository name {} is not allowed".format(repo)) |
| 100 | + |
| 101 | + |
| 102 | +def log_output(stream): |
| 103 | + """Log a stream at DEBUG level, and raise DockerException if it contains "error".""" |
| 104 | + for chunk in stream: |
| 105 | + logger.debug(chunk) |
| 106 | + # error handling requires looking at the response body |
| 107 | + if '"error"' in chunk.lower(): |
| 108 | + raise docker.errors.DockerException(chunk) |
| 109 | + |
| 110 | + |
| 111 | +def strip_prefix(name): |
| 112 | + """Strip the schema and host:port from a Docker repository name.""" |
| 113 | + paths = name.split('/') |
| 114 | + return '/'.join(p for p in paths if p and '.' not in p and ':' not in p) |
| 115 | + |
| 116 | + |
| 117 | +def publish_release(source, config, target, deis_registry): |
| 118 | + |
| 119 | + client = DockerClient() |
| 120 | + return client.publish_release(source, config, target, deis_registry) |
0 commit comments