2014-01-06 03:26:32 +01:00
from __future__ import unicode_literals
from __future__ import absolute_import
2014-04-23 19:20:27 +02:00
from . packages . docker . errors import APIError
2013-12-16 11:51:22 +01:00
import logging
2013-12-09 13:19:27 +01:00
import re
2013-12-19 16:53:39 +01:00
import os
2013-12-18 19:45:25 +01:00
import sys
2013-12-18 19:37:48 +01:00
from . container import Container
2014-05-29 12:19:30 +02:00
from . progress_stream import stream_output , StreamOutputError
2013-12-09 13:19:27 +01:00
2013-12-16 11:51:22 +01:00
log = logging . getLogger ( __name__ )
2013-12-09 13:19:27 +01:00
2013-12-18 17:12:53 +01:00
2014-06-19 12:57:55 +02:00
DOCKER_CONFIG_KEYS = [ ' image ' , ' command ' , ' hostname ' , ' user ' , ' detach ' , ' stdin_open ' , ' tty ' , ' mem_limit ' , ' ports ' , ' environment ' , ' dns ' , ' volumes ' , ' volumes_from ' , ' entrypoint ' , ' privileged ' , ' net ' ]
2014-02-05 01:33:29 +01:00
DOCKER_CONFIG_HINTS = {
2014-03-04 00:51:24 +01:00
' link ' : ' links ' ,
' port ' : ' ports ' ,
' privilege ' : ' privileged ' ,
' priviliged ' : ' privileged ' ,
' privilige ' : ' privileged ' ,
' volume ' : ' volumes ' ,
2014-02-05 01:33:29 +01:00
}
2014-05-02 16:07:20 +02:00
VALID_NAME_CHARS = ' [a-zA-Z0-9] '
2014-02-05 01:33:29 +01:00
2013-12-18 17:12:53 +01:00
class BuildError ( Exception ) :
2014-04-30 12:53:23 +02:00
def __init__ ( self , service , reason ) :
2014-03-25 13:19:42 +01:00
self . service = service
2014-04-30 12:53:23 +02:00
self . reason = reason
2013-12-18 17:12:53 +01:00
2014-01-16 18:58:53 +01:00
class CannotBeScaledError ( Exception ) :
pass
2014-02-05 01:33:29 +01:00
class ConfigError ( ValueError ) :
pass
2013-12-09 12:41:05 +01:00
class Service ( object ) :
2013-12-19 16:16:17 +01:00
def __init__ ( self , name , client = None , project = ' default ' , links = [ ] , * * options ) :
2014-05-02 16:07:20 +02:00
if not re . match ( ' ^ %s +$ ' % VALID_NAME_CHARS , name ) :
raise ConfigError ( ' Invalid service name " %s " - only %s are allowed ' % ( name , VALID_NAME_CHARS ) )
if not re . match ( ' ^ %s +$ ' % VALID_NAME_CHARS , project ) :
raise ConfigError ( ' Invalid project name " %s " - only %s are allowed ' % ( project , VALID_NAME_CHARS ) )
2013-12-13 21:36:10 +01:00
if ' image ' in options and ' build ' in options :
2014-02-05 01:33:29 +01:00
raise ConfigError ( ' Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both. ' % name )
2014-03-04 18:59:42 +01:00
supported_options = DOCKER_CONFIG_KEYS + [ ' build ' , ' expose ' ]
2014-02-05 01:33:29 +01:00
for k in options :
if k not in supported_options :
msg = " Unsupported config option for %s service: ' %s ' " % ( name , k )
if k in DOCKER_CONFIG_HINTS :
msg + = " (did you mean ' %s ' ?) " % DOCKER_CONFIG_HINTS [ k ]
raise ConfigError ( msg )
2013-12-09 13:19:27 +01:00
self . name = name
2013-12-09 12:41:05 +01:00
self . client = client
2013-12-19 16:16:17 +01:00
self . project = project
2013-12-09 15:09:18 +01:00
self . links = links or [ ]
2013-12-10 21:51:55 +01:00
self . options = options
2013-12-09 12:41:05 +01:00
2013-12-20 11:46:55 +01:00
def containers ( self , stopped = False , one_off = False ) :
2013-12-18 19:37:48 +01:00
l = [ ]
2013-12-20 11:46:55 +01:00
for container in self . client . containers ( all = stopped ) :
2013-12-18 15:54:28 +01:00
name = get_container_name ( container )
2013-12-20 13:51:20 +01:00
if not name or not is_valid_name ( name , one_off ) :
2013-12-19 16:16:17 +01:00
continue
project , name , number = parse_name ( name )
if project == self . project and name == self . name :
2013-12-18 19:37:48 +01:00
l . append ( Container . from_ps ( self . client , container ) )
return l
2013-12-09 12:41:05 +01:00
2013-12-20 17:22:54 +01:00
def start ( self , * * options ) :
for c in self . containers ( stopped = True ) :
if not c . is_running :
2014-01-15 19:06:49 +01:00
log . info ( " Starting %s ... " % c . name )
2013-12-20 17:22:54 +01:00
self . start_container ( c , * * options )
2013-12-09 12:41:05 +01:00
2013-12-20 17:22:54 +01:00
def stop ( self , * * options ) :
for c in self . containers ( ) :
2014-01-15 19:06:49 +01:00
log . info ( " Stopping %s ... " % c . name )
2013-12-20 17:22:54 +01:00
c . stop ( * * options )
2013-12-09 12:41:05 +01:00
2013-12-20 17:53:07 +01:00
def kill ( self , * * options ) :
for c in self . containers ( ) :
2014-01-15 19:06:49 +01:00
log . info ( " Killing %s ... " % c . name )
2013-12-20 17:53:07 +01:00
c . kill ( * * options )
2014-01-16 18:58:53 +01:00
def scale ( self , desired_num ) :
2014-03-21 19:34:19 +01:00
"""
Adjusts the number of containers to the specified number and ensures they are running .
- creates containers until there are at least ` desired_num `
- stops containers until there are at most ` desired_num ` running
- starts containers until there are at least ` desired_num ` running
- removes all stopped containers
"""
2014-01-16 18:58:53 +01:00
if not self . can_be_scaled ( ) :
raise CannotBeScaledError ( )
# Create enough containers
containers = self . containers ( stopped = True )
while len ( containers ) < desired_num :
containers . append ( self . create_container ( ) )
running_containers = [ ]
stopped_containers = [ ]
for c in containers :
if c . is_running :
running_containers . append ( c )
else :
stopped_containers . append ( c )
running_containers . sort ( key = lambda c : c . number )
stopped_containers . sort ( key = lambda c : c . number )
# Stop containers
while len ( running_containers ) > desired_num :
c = running_containers . pop ( )
log . info ( " Stopping %s ... " % c . name )
c . stop ( timeout = 1 )
stopped_containers . append ( c )
# Start containers
while len ( running_containers ) < desired_num :
c = stopped_containers . pop ( 0 )
log . info ( " Starting %s ... " % c . name )
2014-02-17 22:33:05 +01:00
self . start_container ( c )
2014-01-16 18:58:53 +01:00
running_containers . append ( c )
2014-03-21 19:34:19 +01:00
self . remove_stopped ( )
2014-01-16 18:58:53 +01:00
2013-12-20 17:53:07 +01:00
def remove_stopped ( self , * * options ) :
for c in self . containers ( stopped = True ) :
if not c . is_running :
2014-01-15 19:06:49 +01:00
log . info ( " Removing %s ... " % c . name )
2013-12-20 17:53:07 +01:00
c . remove ( * * options )
2013-12-20 11:46:55 +01:00
def create_container ( self , one_off = False , * * override_options ) :
2013-12-18 12:37:51 +01:00
"""
Create a container for this service . If the image doesn ' t exist, attempt to pull
it .
"""
2014-03-04 00:51:24 +01:00
container_options = self . _get_container_create_options ( override_options , one_off = one_off )
2013-12-18 12:37:51 +01:00
try :
2013-12-18 19:37:48 +01:00
return Container . create ( self . client , * * container_options )
2014-01-06 03:26:32 +01:00
except APIError as e :
2014-01-06 04:32:06 +01:00
if e . response . status_code == 404 and e . explanation and ' No such image ' in str ( e . explanation ) :
2013-12-18 12:37:51 +01:00
log . info ( ' Pulling image %s ... ' % container_options [ ' image ' ] )
2014-03-25 18:12:59 +01:00
output = self . client . pull ( container_options [ ' image ' ] , stream = True )
stream_output ( output , sys . stdout )
2013-12-18 19:37:48 +01:00
return Container . create ( self . client , * * container_options )
2013-12-18 12:37:51 +01:00
raise
2013-12-17 15:13:12 +01:00
2014-01-03 12:18:59 +01:00
def recreate_containers ( self , * * override_options ) :
"""
2014-04-23 16:46:26 +02:00
If a container for this service doesn ' t exist, create and start one. If there are
any , stop them , create + start new ones , and remove the old containers .
2014-01-03 12:18:59 +01:00
"""
2014-01-15 17:15:46 +01:00
containers = self . containers ( stopped = True )
if len ( containers ) == 0 :
2014-01-15 19:06:49 +01:00
log . info ( " Creating %s ... " % self . next_container_name ( ) )
2014-04-23 16:46:26 +02:00
container = self . create_container ( * * override_options )
self . start_container ( container )
return [ ( None , container ) ]
2014-01-03 12:18:59 +01:00
else :
2014-04-23 16:46:26 +02:00
tuples = [ ]
2014-01-15 17:15:46 +01:00
for c in containers :
2014-01-15 19:06:49 +01:00
log . info ( " Recreating %s ... " % c . name )
2014-04-23 16:46:26 +02:00
tuples . append ( self . recreate_container ( c , * * override_options ) )
2014-01-15 17:15:46 +01:00
2014-04-23 16:46:26 +02:00
return tuples
2014-01-03 12:18:59 +01:00
2014-01-15 17:15:46 +01:00
def recreate_container ( self , container , * * override_options ) :
if container . is_running :
container . stop ( timeout = 1 )
2014-01-20 17:10:54 +01:00
intermediate_container = Container . create (
self . client ,
image = container . image ,
volumes_from = container . id ,
2014-02-21 19:12:51 +01:00
entrypoint = [ ' echo ' ] ,
command = [ ] ,
2014-01-20 17:10:54 +01:00
)
2014-04-23 16:46:26 +02:00
intermediate_container . start ( volumes_from = container . id )
2014-01-15 18:06:16 +01:00
intermediate_container . wait ( )
container . remove ( )
2014-01-15 17:15:46 +01:00
options = dict ( override_options )
2014-01-15 18:06:16 +01:00
options [ ' volumes_from ' ] = intermediate_container . id
new_container = self . create_container ( * * options )
2014-04-23 16:46:26 +02:00
self . start_container ( new_container , volumes_from = intermediate_container . id )
intermediate_container . remove ( )
2014-01-15 17:15:46 +01:00
2014-01-15 18:06:16 +01:00
return ( intermediate_container , new_container )
2014-01-15 17:15:46 +01:00
2014-04-23 16:46:26 +02:00
def start_container ( self , container = None , volumes_from = None , * * override_options ) :
2013-12-17 15:13:12 +01:00
if container is None :
container = self . create_container ( * * override_options )
2013-12-19 13:26:58 +01:00
options = self . options . copy ( )
options . update ( override_options )
2013-12-16 12:22:54 +01:00
port_bindings = { }
2013-12-19 13:26:58 +01:00
if options . get ( ' ports ' , None ) is not None :
for port in options [ ' ports ' ] :
2014-01-06 03:26:32 +01:00
port = str ( port )
2013-12-19 13:26:58 +01:00
if ' : ' in port :
2014-01-16 02:54:05 +01:00
external_port , internal_port = port . split ( ' : ' , 1 )
2013-12-19 13:26:58 +01:00
else :
2014-01-22 18:01:10 +01:00
external_port , internal_port = ( None , port )
port_bindings [ internal_port ] = external_port
2013-12-19 13:26:58 +01:00
2013-12-19 16:53:39 +01:00
volume_bindings = { }
if options . get ( ' volumes ' , None ) is not None :
for volume in options [ ' volumes ' ] :
2014-01-15 13:43:40 +01:00
if ' : ' in volume :
external_dir , internal_dir = volume . split ( ' : ' )
2014-04-25 13:28:00 +02:00
volume_bindings [ os . path . abspath ( external_dir ) ] = {
' bind ' : internal_dir ,
' ro ' : False ,
}
2013-12-19 16:53:39 +01:00
2014-03-04 00:51:24 +01:00
privileged = options . get ( ' privileged ' , False )
2014-06-19 12:57:55 +02:00
net = options . get ( ' net ' , ' bridge ' )
2014-03-04 00:51:24 +01:00
2013-12-18 19:37:48 +01:00
container . start (
2014-03-06 19:59:24 +01:00
links = self . _get_links ( link_to_self = override_options . get ( ' one_off ' , False ) ) ,
2013-12-16 12:22:54 +01:00
port_bindings = port_bindings ,
2013-12-19 16:53:39 +01:00
binds = volume_bindings ,
2014-04-23 16:46:26 +02:00
volumes_from = volumes_from ,
2014-03-04 00:51:24 +01:00
privileged = privileged ,
2014-06-19 12:57:55 +02:00
network_mode = net ,
2013-12-09 22:39:11 +01:00
)
2013-12-17 15:13:12 +01:00
return container
2013-12-09 12:41:05 +01:00
2013-12-20 11:46:55 +01:00
def next_container_name ( self , one_off = False ) :
bits = [ self . project , self . name ]
if one_off :
bits . append ( ' run ' )
2014-01-06 03:26:32 +01:00
return ' _ ' . join ( bits + [ str ( self . next_container_number ( one_off = one_off ) ) ] )
2013-12-19 16:16:17 +01:00
2013-12-20 13:55:45 +01:00
def next_container_number ( self , one_off = False ) :
numbers = [ parse_name ( c . name ) [ 2 ] for c in self . containers ( stopped = True , one_off = one_off ) ]
2013-12-09 16:00:41 +01:00
if len ( numbers ) == 0 :
return 1
else :
return max ( numbers ) + 1
2014-03-06 19:59:24 +01:00
def _get_links ( self , link_to_self ) :
2014-01-27 16:29:58 +01:00
links = [ ]
2014-03-01 17:17:19 +01:00
for service , link_name in self . links :
2013-12-18 19:37:48 +01:00
for container in service . containers ( ) :
2014-03-01 17:17:19 +01:00
if link_name :
links . append ( ( container . name , link_name ) )
2014-01-27 16:29:58 +01:00
links . append ( ( container . name , container . name ) )
links . append ( ( container . name , container . name_without_project ) )
2014-03-06 19:59:24 +01:00
if link_to_self :
for container in self . containers ( ) :
links . append ( ( container . name , container . name ) )
links . append ( ( container . name , container . name_without_project ) )
2013-12-09 22:39:11 +01:00
return links
2014-03-04 00:51:24 +01:00
def _get_container_create_options ( self , override_options , one_off = False ) :
2014-02-05 01:33:29 +01:00
container_options = dict ( ( k , self . options [ k ] ) for k in DOCKER_CONFIG_KEYS if k in self . options )
2013-12-13 21:36:10 +01:00
container_options . update ( override_options )
2013-12-20 11:46:55 +01:00
container_options [ ' name ' ] = self . next_container_name ( one_off )
2013-12-13 21:36:10 +01:00
2014-03-04 18:59:42 +01:00
if ' ports ' in container_options or ' expose ' in self . options :
2014-01-16 02:54:05 +01:00
ports = [ ]
2014-03-04 18:59:42 +01:00
all_ports = container_options . get ( ' ports ' , [ ] ) + self . options . get ( ' expose ' , [ ] )
for port in all_ports :
2014-01-16 02:54:05 +01:00
port = str ( port )
if ' : ' in port :
port = port . split ( ' : ' ) [ - 1 ]
2014-01-22 18:01:10 +01:00
if ' / ' in port :
port = tuple ( port . split ( ' / ' ) )
2014-01-16 02:54:05 +01:00
ports . append ( port )
container_options [ ' ports ' ] = ports
2013-12-18 12:14:14 +01:00
2013-12-19 16:53:39 +01:00
if ' volumes ' in container_options :
2014-01-15 13:43:40 +01:00
container_options [ ' volumes ' ] = dict ( ( split_volume ( v ) [ 1 ] , { } ) for v in container_options [ ' volumes ' ] )
2013-12-19 16:53:39 +01:00
2014-01-02 16:28:33 +01:00
if self . can_be_built ( ) :
2013-12-20 17:23:40 +01:00
if len ( self . client . images ( name = self . _build_tag_name ( ) ) ) == 0 :
self . build ( )
container_options [ ' image ' ] = self . _build_tag_name ( )
2013-12-13 21:36:10 +01:00
2014-03-04 00:51:24 +01:00
# Priviliged is only required for starting containers, not for creating them
if ' privileged ' in container_options :
del container_options [ ' privileged ' ]
2014-06-19 12:57:55 +02:00
# net is only required for starting containers, not for creating them
if ' net ' in container_options :
del container_options [ ' net ' ]
2013-12-13 21:36:10 +01:00
return container_options
2013-12-09 22:39:11 +01:00
2013-12-18 17:12:53 +01:00
def build ( self ) :
2013-12-18 19:46:53 +01:00
log . info ( ' Building %s ... ' % self . name )
2013-12-18 17:12:53 +01:00
2013-12-20 17:23:40 +01:00
build_output = self . client . build (
self . options [ ' build ' ] ,
tag = self . _build_tag_name ( ) ,
2014-05-01 16:41:36 +02:00
stream = True ,
rm = True
2013-12-20 17:23:40 +01:00
)
2013-12-18 17:12:53 +01:00
2014-04-30 12:53:23 +02:00
try :
all_events = stream_output ( build_output , sys . stdout )
except StreamOutputError , e :
raise BuildError ( self , unicode ( e ) )
2014-03-25 18:12:59 +01:00
2013-12-18 17:12:53 +01:00
image_id = None
2014-03-25 18:12:59 +01:00
for event in all_events :
if ' stream ' in event :
match = re . search ( r ' Successfully built ([0-9a-f]+) ' , event . get ( ' stream ' , ' ' ) )
2013-12-18 17:12:53 +01:00
if match :
image_id = match . group ( 1 )
if image_id is None :
2014-03-25 13:19:42 +01:00
raise BuildError ( self )
2013-12-18 17:12:53 +01:00
return image_id
2014-01-02 16:28:33 +01:00
def can_be_built ( self ) :
return ' build ' in self . options
2013-12-20 17:23:40 +01:00
def _build_tag_name ( self ) :
"""
The tag to give to images built for this service .
"""
return ' %s _ %s ' % ( self . project , self . name )
2014-01-16 18:58:53 +01:00
def can_be_scaled ( self ) :
for port in self . options . get ( ' ports ' , [ ] ) :
if ' : ' in str ( port ) :
return False
return True
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
NAME_RE = re . compile ( r ' ^([^_]+)_([^_]+)_(run_)?( \ d+)$ ' )
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
def is_valid_name ( name , one_off = False ) :
match = NAME_RE . match ( name )
if match is None :
return False
if one_off :
return match . group ( 3 ) == ' run_ '
else :
return match . group ( 3 ) is None
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
def parse_name ( name , one_off = False ) :
2013-12-19 21:09:54 +01:00
match = NAME_RE . match ( name )
2013-12-20 11:46:55 +01:00
( project , service_name , _ , suffix ) = match . groups ( )
2013-12-19 16:16:17 +01:00
return ( project , service_name , int ( suffix ) )
2013-12-09 16:00:41 +01:00
def get_container_name ( container ) :
2013-12-20 13:51:20 +01:00
if not container . get ( ' Name ' ) and not container . get ( ' Names ' ) :
return None
2013-12-18 17:12:53 +01:00
# inspect
if ' Name ' in container :
return container [ ' Name ' ]
# ps
2013-12-17 13:12:13 +01:00
for name in container [ ' Names ' ] :
if len ( name . split ( ' / ' ) ) == 2 :
return name [ 1 : ]
2014-01-15 13:43:40 +01:00
def split_volume ( v ) :
"""
If v is of the format EXTERNAL : INTERNAL , returns ( EXTERNAL , INTERNAL ) .
If v is of the format INTERNAL , returns ( None , INTERNAL ) .
"""
if ' : ' in v :
return v . split ( ' : ' , 1 )
else :
return ( None , v )