2014-01-06 04:07:44 +01:00
from __future__ import print_function
2014-01-06 04:52:56 +01:00
from __future__ import unicode_literals
2013-12-11 15:25:32 +01:00
import logging
2013-12-19 14:06:26 +01:00
import sys
2013-12-11 15:25:32 +01:00
import re
2014-01-02 20:18:08 +01:00
import signal
2013-12-11 15:25:32 +01:00
from inspect import getdoc
from . . import __version__
2014-03-03 17:21:42 +01:00
from . . project import NoSuchService , ConfigurationError
2014-01-16 18:58:53 +01:00
from . . service import CannotBeScaledError
2013-12-11 15:25:32 +01:00
from . command import Command
2013-12-19 14:02:04 +01:00
from . formatter import Formatter
2013-12-18 15:58:58 +01:00
from . log_printer import LogPrinter
2013-12-31 14:42:58 +01:00
from . utils import yesno
2013-12-11 15:25:32 +01:00
2014-01-16 13:56:36 +01:00
from . . packages . docker . client import APIError
2013-12-11 15:25:32 +01:00
from . errors import UserError
from . docopt_command import NoSuchCommand
2013-12-20 16:03:01 +01:00
from . socketclient import SocketClient
2013-12-11 15:25:32 +01:00
log = logging . getLogger ( __name__ )
2013-12-13 20:19:44 +01:00
2013-12-11 15:25:32 +01:00
def main ( ) :
2014-01-02 19:30:47 +01:00
console_handler = logging . StreamHandler ( stream = sys . stderr )
2013-12-13 20:19:44 +01:00
console_handler . setFormatter ( logging . Formatter ( ) )
console_handler . setLevel ( logging . INFO )
root_logger = logging . getLogger ( )
root_logger . addHandler ( console_handler )
root_logger . setLevel ( logging . DEBUG )
# Disable requests logging
logging . getLogger ( " requests " ) . propagate = False
2013-12-11 15:25:32 +01:00
try :
command = TopLevelCommand ( )
command . sys_dispatch ( )
except KeyboardInterrupt :
log . error ( " \n Aborting. " )
2014-02-19 23:42:21 +01:00
sys . exit ( 1 )
2014-03-03 17:21:42 +01:00
except ( UserError , NoSuchService , ConfigurationError ) as e :
2013-12-20 19:30:23 +01:00
log . error ( e . msg )
2014-02-19 23:42:21 +01:00
sys . exit ( 1 )
2014-01-06 04:06:12 +01:00
except NoSuchCommand as e :
2013-12-11 15:25:32 +01:00
log . error ( " No such command: %s " , e . command )
log . error ( " " )
log . error ( " \n " . join ( parse_doc_section ( " commands: " , getdoc ( e . supercommand ) ) ) )
2014-02-19 23:42:21 +01:00
sys . exit ( 1 )
2014-01-06 04:06:12 +01:00
except APIError as e :
2013-12-19 13:36:38 +01:00
log . error ( e . explanation )
2014-02-19 23:42:21 +01:00
sys . exit ( 1 )
2013-12-11 15:25:32 +01:00
# stolen from docopt master
def parse_doc_section ( name , source ) :
pattern = re . compile ( ' ^([^ \n ]* ' + name + ' [^ \n ]* \n ?(?:[ \t ].*?(?: \n |$))*) ' ,
re . IGNORECASE | re . MULTILINE )
return [ s . strip ( ) for s in pattern . findall ( source ) ]
class TopLevelCommand ( Command ) :
2013-12-20 22:34:27 +01:00
""" Punctual, lightweight development environments using Docker.
2013-12-11 15:25:32 +01:00
Usage :
2013-12-20 21:28:24 +01:00
fig [ options ] [ COMMAND ] [ ARGS . . . ]
fig - h | - - help
2013-12-11 15:25:32 +01:00
Options :
- - verbose Show more output
- - version Print version and exit
2014-02-26 16:45:14 +01:00
- f , - - file FILE Specify an alternate fig file ( default : fig . yml )
2013-12-11 15:25:32 +01:00
Commands :
2014-01-02 16:28:33 +01:00
build Build or rebuild services
2014-01-16 14:24:43 +01:00
help Get help on a command
2013-12-31 18:05:20 +01:00
kill Kill containers
2013-12-20 11:57:28 +01:00
logs View output from containers
2013-12-20 20:15:12 +01:00
ps List containers
2013-12-31 18:05:20 +01:00
rm Remove stopped containers
2013-12-13 21:55:28 +01:00
run Run a one - off command
2014-01-16 18:58:53 +01:00
scale Set number of containers for a service
2013-12-13 21:35:54 +01:00
start Start services
stop Stop services
2013-12-31 18:05:20 +01:00
up Create and start containers
2013-12-11 15:25:32 +01:00
"""
2013-12-19 14:06:26 +01:00
def docopt_options ( self ) :
options = super ( TopLevelCommand , self ) . docopt_options ( )
2013-12-20 21:28:24 +01:00
options [ ' version ' ] = " fig %s " % __version__
2013-12-19 14:06:26 +01:00
return options
2014-01-02 16:28:33 +01:00
def build ( self , options ) :
"""
Build or rebuild services .
2014-01-16 14:17:00 +01:00
Services are built once and then tagged as ` project_service ` ,
e . g . ` figtest_db ` . If you change a service ' s `Dockerfile` or the
contents of its build directory , you can run ` fig build ` to rebuild it .
2014-01-02 16:28:33 +01:00
Usage : build [ SERVICE . . . ]
"""
self . project . build ( service_names = options [ ' SERVICE ' ] )
2014-01-16 14:24:43 +01:00
def help ( self , options ) :
"""
Get help on a command .
Usage : help COMMAND
"""
command = options [ ' COMMAND ' ]
if not hasattr ( self , command ) :
raise NoSuchCommand ( command , self )
raise SystemExit ( getdoc ( getattr ( self , command ) ) )
2013-12-31 18:05:20 +01:00
def kill ( self , options ) :
"""
2014-01-16 14:17:00 +01:00
Force stop service containers .
2013-12-31 18:05:20 +01:00
Usage : kill [ SERVICE . . . ]
"""
self . project . kill ( service_names = options [ ' SERVICE ' ] )
def logs ( self , options ) :
"""
View output from containers .
Usage : logs [ SERVICE . . . ]
"""
2014-01-14 13:39:13 +01:00
containers = self . project . containers ( service_names = options [ ' SERVICE ' ] , stopped = True )
2014-01-06 04:07:21 +01:00
print ( " Attaching to " , list_containers ( containers ) )
2013-12-31 18:05:20 +01:00
LogPrinter ( containers , attach_params = { ' logs ' : True } ) . run ( )
2013-12-11 15:25:32 +01:00
def ps ( self , options ) :
"""
2013-12-20 20:15:12 +01:00
List containers .
2013-12-11 15:25:32 +01:00
2013-12-20 20:13:55 +01:00
Usage : ps [ options ] [ SERVICE . . . ]
2013-12-19 14:02:04 +01:00
Options :
- q Only display IDs
2013-12-11 15:25:32 +01:00
"""
2013-12-20 20:13:55 +01:00
containers = self . project . containers ( service_names = options [ ' SERVICE ' ] , stopped = True ) + self . project . containers ( service_names = options [ ' SERVICE ' ] , one_off = True )
2013-12-20 11:46:55 +01:00
2013-12-19 14:02:04 +01:00
if options [ ' -q ' ] :
2013-12-20 11:46:55 +01:00
for container in containers :
2014-01-06 04:07:21 +01:00
print ( container . id )
2013-12-19 14:02:04 +01:00
else :
headers = [
' Name ' ,
' Command ' ,
' State ' ,
' Ports ' ,
]
rows = [ ]
2013-12-20 11:46:55 +01:00
for container in containers :
2014-01-16 15:02:52 +01:00
command = container . human_readable_command
if len ( command ) > 30 :
2014-01-16 15:06:48 +01:00
command = ' %s ... ' % command [ : 26 ]
2013-12-19 14:02:04 +01:00
rows . append ( [
container . name ,
2014-01-16 15:02:52 +01:00
command ,
2013-12-19 14:02:04 +01:00
container . human_readable_state ,
container . human_readable_ports ,
] )
2014-01-06 04:07:21 +01:00
print ( Formatter ( ) . table ( headers , rows ) )
2013-12-11 15:25:32 +01:00
2013-12-31 18:05:20 +01:00
def rm ( self , options ) :
"""
2014-01-16 14:17:00 +01:00
Remove stopped service containers .
2013-12-31 18:05:20 +01:00
2014-03-04 11:25:50 +01:00
Usage : rm [ options ] [ SERVICE . . . ]
2014-03-03 10:55:00 +01:00
Options :
2014-03-04 11:25:50 +01:00
- - force Don ' t ask to confirm removal
- v Remove volumes associated with containers
2013-12-31 18:05:20 +01:00
"""
all_containers = self . project . containers ( service_names = options [ ' SERVICE ' ] , stopped = True )
stopped_containers = [ c for c in all_containers if not c . is_running ]
if len ( stopped_containers ) > 0 :
2014-01-06 04:07:21 +01:00
print ( " Going to remove " , list_containers ( stopped_containers ) )
2014-03-04 11:25:50 +01:00
if options . get ( ' --force ' ) \
or yesno ( " Are you sure? [yN] " , default = False ) :
2014-03-03 10:55:00 +01:00
self . project . remove_stopped ( service_names = options [ ' SERVICE ' ] ,
2014-03-04 06:13:23 +01:00
remove_volumes = options . get ( ' -v ' , False ) )
2013-12-31 18:05:20 +01:00
else :
2014-01-06 04:07:21 +01:00
print ( " No stopped containers " )
2013-12-31 18:05:20 +01:00
2013-12-13 21:55:28 +01:00
def run ( self , options ) :
"""
2014-01-16 14:17:00 +01:00
Run a one - off command on a service .
For example :
$ fig run web python manage . py shell
Note that this will not start any services that the command ' s service
links to . So if , for example , your one - off command talks to your
database , you will need to run ` fig up - d db ` first .
2013-12-13 21:55:28 +01:00
2013-12-20 11:53:07 +01:00
Usage : run [ options ] SERVICE COMMAND [ ARGS . . . ]
Options :
2014-01-19 21:33:06 +01:00
- d Detached mode : Run container in the background , print new
container name
- T Disable pseudo - tty allocation . By default ` fig run `
allocates a TTY .
2013-12-13 21:55:28 +01:00
"""
2013-12-19 17:55:12 +01:00
service = self . project . get_service ( options [ ' SERVICE ' ] )
2014-01-19 21:33:06 +01:00
tty = True
if options [ ' -d ' ] or options [ ' -T ' ] or not sys . stdin . isatty ( ) :
tty = False
2013-12-17 15:13:12 +01:00
container_options = {
' command ' : [ options [ ' COMMAND ' ] ] + options [ ' ARGS ' ] ,
2014-01-19 21:33:06 +01:00
' tty ' : tty ,
2013-12-20 16:03:01 +01:00
' stdin_open ' : not options [ ' -d ' ] ,
2013-12-17 15:13:12 +01:00
}
2013-12-20 11:46:55 +01:00
container = service . create_container ( one_off = True , * * container_options )
2013-12-20 11:53:07 +01:00
if options [ ' -d ' ] :
service . start_container ( container , ports = None )
2014-01-06 04:07:21 +01:00
print ( container . name )
2013-12-20 11:53:07 +01:00
else :
2014-01-20 16:52:07 +01:00
with self . _attach_to_container ( container . id , raw = tty ) as c :
2013-12-20 16:03:01 +01:00
service . start_container ( container , ports = None )
c . run ( )
2013-12-13 21:55:28 +01:00
2014-01-16 18:58:53 +01:00
def scale ( self , options ) :
"""
Set number of containers to run for a service .
Numbers are specified in the form ` service = num ` as arguments .
For example :
$ fig scale web = 2 worker = 3
Usage : scale [ SERVICE = NUM . . . ]
"""
for s in options [ ' SERVICE=NUM ' ] :
if ' = ' not in s :
raise UserError ( ' Arguments to scale should be in the form service=num ' )
service_name , num = s . split ( ' = ' , 1 )
try :
num = int ( num )
except ValueError :
raise UserError ( ' Number of containers for service " %s " is not a number ' % service )
try :
self . project . get_service ( service_name ) . scale ( num )
except CannotBeScaledError :
raise UserError ( ' Service " %s " cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash. \n \n Remove the " : " from the port definition in fig.yml so Docker can choose a random port for each container. ' % service_name )
2013-12-31 18:05:20 +01:00
def start ( self , options ) :
"""
Start existing containers .
Usage : start [ SERVICE . . . ]
"""
self . project . start ( service_names = options [ ' SERVICE ' ] )
def stop ( self , options ) :
"""
2014-01-16 14:17:00 +01:00
Stop running containers without removing them .
They can be started again with ` fig start ` .
2013-12-31 18:05:20 +01:00
Usage : stop [ SERVICE . . . ]
"""
self . project . stop ( service_names = options [ ' SERVICE ' ] )
2013-12-20 17:22:54 +01:00
def up ( self , options ) :
2013-12-11 15:25:32 +01:00
"""
2014-01-16 14:17:00 +01:00
Build , ( re ) create , start and attach to containers for a service .
By default , ` fig up ` will aggregate the output of each container , and
when it exits , all containers will be stopped . If you run ` fig up - d ` ,
it ' ll start the containers in the background and leave them running.
If there are existing containers for a service , ` fig up ` will stop
and recreate them ( preserving mounted volumes with volumes - from ) ,
so that changes in ` fig . yml ` are picked up .
2013-12-11 15:25:32 +01:00
2013-12-20 19:30:23 +01:00
Usage : up [ options ] [ SERVICE . . . ]
2013-12-20 11:53:07 +01:00
Options :
2014-01-16 14:17:00 +01:00
- d Detached mode : Run containers in the background , print new
container names
2013-12-11 15:25:32 +01:00
"""
2013-12-20 17:22:54 +01:00
detached = options [ ' -d ' ]
2013-12-18 20:01:53 +01:00
2014-01-15 14:25:40 +01:00
( old , new ) = self . project . recreate_containers ( service_names = options [ ' SERVICE ' ] )
2013-12-18 20:01:53 +01:00
2013-12-20 17:22:54 +01:00
if not detached :
2014-01-15 14:25:40 +01:00
to_attach = [ c for ( s , c ) in new ]
print ( " Attaching to " , list_containers ( to_attach ) )
2014-02-06 02:19:18 +01:00
log_printer = LogPrinter ( to_attach , attach_params = { " logs " : True } )
2013-12-18 20:01:53 +01:00
2014-01-15 14:25:40 +01:00
for ( service , container ) in new :
service . start_container ( container )
for ( service , container ) in old :
container . remove ( )
2013-12-18 20:01:53 +01:00
2013-12-31 14:02:08 +01:00
if not detached :
2013-12-20 17:22:54 +01:00
try :
log_printer . run ( )
finally :
2014-01-02 20:18:08 +01:00
def handler ( signal , frame ) :
self . project . kill ( service_names = options [ ' SERVICE ' ] )
sys . exit ( 0 )
signal . signal ( signal . SIGINT , handler )
2014-01-06 04:07:21 +01:00
print ( " Gracefully stopping... (press Ctrl+C again to force) " )
2014-01-02 20:18:08 +01:00
self . project . stop ( service_names = options [ ' SERVICE ' ] )
2013-12-20 17:22:54 +01:00
2014-01-20 16:52:07 +01:00
def _attach_to_container ( self , container_id , raw = False ) :
socket_in = self . client . attach_socket ( container_id , params = { ' stdin ' : 1 , ' stream ' : 1 } )
socket_out = self . client . attach_socket ( container_id , params = { ' stdout ' : 1 , ' logs ' : 1 , ' stream ' : 1 } )
socket_err = self . client . attach_socket ( container_id , params = { ' stderr ' : 1 , ' logs ' : 1 , ' stream ' : 1 } )
2013-12-20 16:03:01 +01:00
return SocketClient (
2014-01-20 16:52:07 +01:00
socket_in = socket_in ,
socket_out = socket_out ,
socket_err = socket_err ,
2013-12-20 16:03:01 +01:00
raw = raw ,
)
2013-12-18 20:01:53 +01:00
def list_containers ( containers ) :
return " , " . join ( c . name for c in containers )