| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 | from __future__ import absolute_importfrom __future__ import unicode_literalsimport jsonimport loggingimport sixfrom docker.utils import split_commandfrom docker.utils.ports import split_portfrom .cli.errors import UserErrorfrom .config.serialize import denormalize_configfrom .network import get_network_defs_for_servicefrom .service import format_environmentfrom .service import NoSuchImageErrorfrom .service import parse_repository_taglog = 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_nameclass NeedsPull(Exception):    def __init__(self, image_name, service_name):        self.image_name = image_name        self.service_name = service_nameclass MissingDigests(Exception):    def __init__(self, needs_push, needs_pull):        self.needs_push = needs_push        self.needs_pull = needs_pulldef serialize_bundle(config, image_digests):    return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)def get_image_digests(project, allow_push=False):    digests = {}    needs_push = set()    needs_pull = set()    for service in project.services:        try:            digests[service.name] = get_image_digest(                service,                allow_push=allow_push,            )        except NeedsPush as e:            needs_push.add(e.image_name)        except NeedsPull as e:            needs_pull.add(e.service_name)    if needs_push or needs_pull:        raise MissingDigests(needs_push, needs_pull)    return digestsdef get_image_digest(service, allow_push=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']    digest = get_digest(service)    if digest:        return digest    if 'build' not in service.options:        raise NeedsPull(service.image_name, service.name)    if not allow_push:        raise NeedsPush(service.image_name)    return push_image(service)def get_digest(service):    digest = None    try:        image = service.image()        # TODO: pick a digest based on the image tag if there are multiple        # digests        if image['RepoDigests']:            digest = image['RepoDigests'][0]    except NoSuchImageError:        try:            # Fetch the image digest from the registry            distribution = service.get_image_registry_data()            if distribution['Descriptor']['digest']:                digest = '{image_name}@{digest}'.format(                    image_name=service.image_name,                    digest=distribution['Descriptor']['digest']                )        except NoSuchImageError:            raise UserError(                "Digest not found for service '{service}'. "                "Repository does not exist or may require 'docker login'"                .format(service=service.name))    return digestdef push_image(service):    try:        digest = service.push()    except Exception:        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 identifierdef to_bundle(config, image_digests):    if config.networks:        log.warning("Unsupported top level key 'networks' - ignoring")    if config.volumes:        log.warning("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.warning("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#L95def 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'] = commanddef 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.warning(                "Unsupported key '{}' in services.{}.networks.{} - ignoring"                .format(key, name, network_name))        networks.append(network_name)    return networksdef 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 portsdef make_port_spec(value):    components = six.text_type(value).partition('/')    return {        'Protocol': components[2] or 'tcp',        'Port': int(components[0]),    }
 |