123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- from __future__ import absolute_import
- from __future__ import unicode_literals
- import json
- import logging
- import six
- from docker.utils import split_command
- from docker.utils.ports import split_port
- from .cli.errors import UserError
- from .config.serialize import denormalize_config
- from .network import get_network_defs_for_service
- from .service import format_environment
- from .service import NoSuchImageError
- from .service import parse_repository_tag
- log = logging.getLogger(__name__)
- SERVICE_KEYS = {
- 'working_dir': 'WorkingDir',
- 'user': 'User',
- 'labels': 'Labels',
- }
- IGNORED_KEYS = {'build'}
- SUPPORTED_KEYS = {
- 'image',
- 'ports',
- 'expose',
- 'networks',
- 'command',
- 'environment',
- 'entrypoint',
- } | set(SERVICE_KEYS)
- VERSION = '0.1'
- class NeedsPush(Exception):
- def __init__(self, image_name):
- self.image_name = image_name
- class NeedsPull(Exception):
- def __init__(self, image_name):
- self.image_name = image_name
- class MissingDigests(Exception):
- def __init__(self, needs_push, needs_pull):
- self.needs_push = needs_push
- self.needs_pull = needs_pull
- def serialize_bundle(config, image_digests):
- return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
- def get_image_digests(project, allow_fetch=False):
- digests = {}
- needs_push = set()
- needs_pull = set()
- for service in project.services:
- try:
- digests[service.name] = get_image_digest(
- service,
- allow_fetch=allow_fetch,
- )
- except NeedsPush as e:
- needs_push.add(e.image_name)
- except NeedsPull as e:
- needs_pull.add(e.image_name)
- if needs_push or needs_pull:
- raise MissingDigests(needs_push, needs_pull)
- return digests
- def get_image_digest(service, allow_fetch=False):
- if 'image' not in service.options:
- raise UserError(
- "Service '{s.name}' doesn't define an image tag. An image name is "
- "required to generate a proper image digest for the bundle. Specify "
- "an image repo and tag with the 'image' option.".format(s=service))
- _, _, separator = parse_repository_tag(service.options['image'])
- # Compose file already uses a digest, no lookup required
- if separator == '@':
- return service.options['image']
- try:
- image = service.image()
- except NoSuchImageError:
- action = 'build' if 'build' in service.options else 'pull'
- raise UserError(
- "Image not found for service '{service}'. "
- "You might need to run `docker-compose {action} {service}`."
- .format(service=service.name, action=action))
- if image['RepoDigests']:
- # TODO: pick a digest based on the image tag if there are multiple
- # digests
- return image['RepoDigests'][0]
- if not allow_fetch:
- if 'build' in service.options:
- raise NeedsPush(service.image_name)
- else:
- raise NeedsPull(service.image_name)
- return fetch_image_digest(service)
- def fetch_image_digest(service):
- if 'build' not in service.options:
- digest = service.pull()
- else:
- try:
- digest = service.push()
- except:
- log.error(
- "Failed to push image for service '{s.name}'. Please use an "
- "image tag that can be pushed to a Docker "
- "registry.".format(s=service))
- raise
- if not digest:
- raise ValueError("Failed to get digest for %s" % service.name)
- repo, _, _ = parse_repository_tag(service.options['image'])
- identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
- # only do this if RepoDigests isn't already populated
- image = service.image()
- if not image['RepoDigests']:
- # Pull by digest so that image['RepoDigests'] is populated for next time
- # and we don't have to pull/push again
- service.client.pull(identifier)
- log.info("Stored digest for {}".format(service.image_name))
- return identifier
- def to_bundle(config, image_digests):
- if config.networks:
- log.warn("Unsupported top level key 'networks' - ignoring")
- if config.volumes:
- log.warn("Unsupported top level key 'volumes' - ignoring")
- config = denormalize_config(config)
- return {
- 'Version': VERSION,
- 'Services': {
- name: convert_service_to_bundle(
- name,
- service_dict,
- image_digests[name],
- )
- for name, service_dict in config['services'].items()
- },
- }
- def convert_service_to_bundle(name, service_dict, image_digest):
- container_config = {'Image': image_digest}
- for key, value in service_dict.items():
- if key in IGNORED_KEYS:
- continue
- if key not in SUPPORTED_KEYS:
- log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name))
- continue
- if key == 'environment':
- container_config['Env'] = format_environment({
- envkey: envvalue for envkey, envvalue in value.items()
- if envvalue
- })
- continue
- if key in SERVICE_KEYS:
- container_config[SERVICE_KEYS[key]] = value
- continue
- set_command_and_args(
- container_config,
- service_dict.get('entrypoint', []),
- service_dict.get('command', []))
- container_config['Networks'] = make_service_networks(name, service_dict)
- ports = make_port_specs(service_dict)
- if ports:
- container_config['Ports'] = ports
- return container_config
- # See https://github.com/docker/swarmkit/blob//agent/exec/container/container.go#L95
- def set_command_and_args(config, entrypoint, command):
- if isinstance(entrypoint, six.string_types):
- entrypoint = split_command(entrypoint)
- if isinstance(command, six.string_types):
- command = split_command(command)
- if entrypoint:
- config['Command'] = entrypoint + command
- return
- if command:
- config['Args'] = command
- def make_service_networks(name, service_dict):
- networks = []
- for network_name, network_def in get_network_defs_for_service(service_dict).items():
- for key in network_def.keys():
- log.warn(
- "Unsupported key '{}' in services.{}.networks.{} - ignoring"
- .format(key, name, network_name))
- networks.append(network_name)
- return networks
- def make_port_specs(service_dict):
- ports = []
- internal_ports = [
- internal_port
- for port_def in service_dict.get('ports', [])
- for internal_port in split_port(port_def)[0]
- ]
- internal_ports += service_dict.get('expose', [])
- for internal_port in internal_ports:
- spec = make_port_spec(internal_port)
- if spec not in ports:
- ports.append(spec)
- return ports
- def make_port_spec(value):
- components = six.text_type(value).partition('/')
- return {
- 'Protocol': components[2] or 'tcp',
- 'Port': int(components[0]),
- }
|