|  | @@ -11,6 +11,8 @@ from os import path
 | 
	
		
			
				|  |  |  import enum
 | 
	
		
			
				|  |  |  import six
 | 
	
		
			
				|  |  |  from docker.errors import APIError
 | 
	
		
			
				|  |  | +from docker.errors import ImageNotFound
 | 
	
		
			
				|  |  | +from docker.errors import NotFound
 | 
	
		
			
				|  |  |  from docker.utils import version_lt
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from . import parallel
 | 
	
	
		
			
				|  | @@ -25,6 +27,7 @@ from .container import Container
 | 
	
		
			
				|  |  |  from .network import build_networks
 | 
	
		
			
				|  |  |  from .network import get_networks
 | 
	
		
			
				|  |  |  from .network import ProjectNetworks
 | 
	
		
			
				|  |  | +from .progress_stream import read_status
 | 
	
		
			
				|  |  |  from .service import BuildAction
 | 
	
		
			
				|  |  |  from .service import ContainerNetworkMode
 | 
	
		
			
				|  |  |  from .service import ContainerPidMode
 | 
	
	
		
			
				|  | @@ -619,46 +622,68 @@ class Project(object):
 | 
	
		
			
				|  |  |      def pull(self, service_names=None, ignore_pull_failures=False, parallel_pull=False, silent=False,
 | 
	
		
			
				|  |  |               include_deps=False):
 | 
	
		
			
				|  |  |          services = self.get_services(service_names, include_deps)
 | 
	
		
			
				|  |  | -        msg = not silent and 'Pulling' or None
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if parallel_pull:
 | 
	
		
			
				|  |  | -            def pull_service(service):
 | 
	
		
			
				|  |  | -                strm = service.pull(ignore_pull_failures, True, stream=True)
 | 
	
		
			
				|  |  | -                if strm is None:  # Attempting to pull service with no `image` key is a no-op
 | 
	
		
			
				|  |  | -                    return
 | 
	
		
			
				|  |  | +            self.parallel_pull(services, silent=silent)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                writer = parallel.get_stream_writer()
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            must_build = []
 | 
	
		
			
				|  |  | +            for service in services:
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    service.pull(ignore_pull_failures, silent=silent)
 | 
	
		
			
				|  |  | +                except (ImageNotFound, NotFound):
 | 
	
		
			
				|  |  | +                    if service.can_be_built():
 | 
	
		
			
				|  |  | +                        must_build.append(service.name)
 | 
	
		
			
				|  |  | +                    else:
 | 
	
		
			
				|  |  | +                        raise
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if len(must_build):
 | 
	
		
			
				|  |  | +                log.warning('Some service image(s) must be built from source by running:\n'
 | 
	
		
			
				|  |  | +                            '    docker-compose build {}'
 | 
	
		
			
				|  |  | +                            .format(' '.join(must_build)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def parallel_pull(self, services, ignore_pull_failures=False, silent=False):
 | 
	
		
			
				|  |  | +        msg = 'Pulling' if not silent else None
 | 
	
		
			
				|  |  | +        must_build = []
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        def pull_service(service):
 | 
	
		
			
				|  |  | +            strm = service.pull(ignore_pull_failures, True, stream=True)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +            if strm is None:  # Attempting to pull service with no `image` key is a no-op
 | 
	
		
			
				|  |  | +                return
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            try:
 | 
	
		
			
				|  |  | +                writer = parallel.get_stream_writer()
 | 
	
		
			
				|  |  |                  for event in strm:
 | 
	
		
			
				|  |  |                      if 'status' not in event:
 | 
	
		
			
				|  |  |                          continue
 | 
	
		
			
				|  |  | -                    status = event['status'].lower()
 | 
	
		
			
				|  |  | -                    if 'progressDetail' in event:
 | 
	
		
			
				|  |  | -                        detail = event['progressDetail']
 | 
	
		
			
				|  |  | -                        if 'current' in detail and 'total' in detail:
 | 
	
		
			
				|  |  | -                            percentage = float(detail['current']) / float(detail['total'])
 | 
	
		
			
				|  |  | -                            status = '{} ({:.1%})'.format(status, percentage)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +                    status = read_status(event)
 | 
	
		
			
				|  |  |                      writer.write(
 | 
	
		
			
				|  |  |                          msg, service.name, truncate_string(status), lambda s: s
 | 
	
		
			
				|  |  |                      )
 | 
	
		
			
				|  |  | +            except (ImageNotFound, NotFound):
 | 
	
		
			
				|  |  | +                if service.can_be_built():
 | 
	
		
			
				|  |  | +                    must_build.append(service.name)
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    raise
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            _, errors = parallel.parallel_execute(
 | 
	
		
			
				|  |  | -                services,
 | 
	
		
			
				|  |  | -                pull_service,
 | 
	
		
			
				|  |  | -                operator.attrgetter('name'),
 | 
	
		
			
				|  |  | -                msg,
 | 
	
		
			
				|  |  | -                limit=5,
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | -            if len(errors):
 | 
	
		
			
				|  |  | -                combined_errors = '\n'.join([
 | 
	
		
			
				|  |  | -                    e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values()
 | 
	
		
			
				|  |  | -                ])
 | 
	
		
			
				|  |  | -                raise ProjectError(combined_errors)
 | 
	
		
			
				|  |  | +        _, errors = parallel.parallel_execute(
 | 
	
		
			
				|  |  | +            services,
 | 
	
		
			
				|  |  | +            pull_service,
 | 
	
		
			
				|  |  | +            operator.attrgetter('name'),
 | 
	
		
			
				|  |  | +            msg,
 | 
	
		
			
				|  |  | +            limit=5,
 | 
	
		
			
				|  |  | +        )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        else:
 | 
	
		
			
				|  |  | -            for service in services:
 | 
	
		
			
				|  |  | -                service.pull(ignore_pull_failures, silent=silent)
 | 
	
		
			
				|  |  | +        if len(must_build):
 | 
	
		
			
				|  |  | +            log.warning('Some service image(s) must be built from source by running:\n'
 | 
	
		
			
				|  |  | +                        '    docker-compose build {}'
 | 
	
		
			
				|  |  | +                        .format(' '.join(must_build)))
 | 
	
		
			
				|  |  | +        if len(errors):
 | 
	
		
			
				|  |  | +            combined_errors = '\n'.join([
 | 
	
		
			
				|  |  | +                e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values()
 | 
	
		
			
				|  |  | +            ])
 | 
	
		
			
				|  |  | +            raise ProjectError(combined_errors)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def push(self, service_names=None, ignore_push_failures=False):
 | 
	
		
			
				|  |  |          unique_images = set()
 |