├── __init__.py ├── setup.cfg ├── CHANGES.txt ├── setup.py ├── .gitignore ├── README.md └── bin └── docker-app /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.0, 2016-12-25 -- Initial release. 2 | v0.1.1, 2016-12-25 -- Update main url of package. 3 | v0.1.2, 2016-12-26 -- Fix: cannot use command unless a docker-app.yml file present. Add exec wrapper. Improve docs. 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="docker-app", 5 | version="0.1.2", 6 | author="Diego A.", 7 | author_email="diego.acuna@mailbox.org", 8 | url="https://github.com/diegoacuna/docker-app", 9 | description="Ease your flow with docker-compose", 10 | license='MIT', 11 | # Dependent packages (distributions) 12 | install_requires=[ 13 | "pyyaml", 14 | ], 15 | scripts=['bin/docker-app'] 16 | ) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Sphinx documentation 50 | docs/_build/ 51 | 52 | # PyBuilder 53 | target/ 54 | 55 | # pyenv 56 | .python-version 57 | 58 | .DS_STORE 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-app 2 | 3 | Ease your flow with docker-compose. 4 | 5 | # Description 6 | 7 | docker-app is a small python script builded as a wrapper of docker-compose and its only goal is to reduce your typing and save your precious time. 8 | 9 | # Installation 10 | 11 | The easy way (using pip): 12 | 13 | ``` 14 | pip install docker-app 15 | ``` 16 | 17 | If everything goes ok, you should have a *docker-app* command 18 | 19 | You also can install the command line utility directly using setuptools: 20 | 21 | ``` 22 | git clone https://github.com/diegoacuna/docker-app.git 23 | cd docker-app 24 | python setup.py install 25 | ``` 26 | 27 | ## Use cases 28 | 29 | By default (as in docker-compose), the name of the main container of an app is assumed to be the name of the actual working directory. You can change this behavior configuring the attribute *main* of the docker-app.yml file: 30 | 31 | ```yaml 32 | main: 'name_of_container' 33 | ``` 34 | 35 | The file docker-app.yml should be placed on the root directory of your app. You can use docker-app without having a docker-app.yml file, docker-app is going to work with the default configuration (as explained above). 36 | 37 | ### A set of dependent apps 38 | 39 | Suppose you have several docker-compose apps that interacts with each other: 40 | 41 | * my_app_1 42 | * my_app_2 43 | * my_app_3 => this app depends on my_app_2 and my_app_1 44 | 45 | **Without docker-app** 46 | 47 | ``` 48 | # on directory of my_app_1 49 | docker-compose up -d 50 | # now on directory of my_app_2 51 | docker-compose up -d 52 | # now on directory of my_app_3 53 | docker-compose up -d 54 | ``` 55 | 56 | **With docker-app** 57 | 58 | You define a set of dependencies in my_app_3 using a docker-app.yml file: 59 | 60 | ```yaml 61 | dependencies: 62 | - my_app_1 63 | - my_app_2 64 | ``` 65 | 66 | and now: 67 | 68 | ``` 69 | docker-app up 70 | ``` 71 | 72 | docker-app is going to do a 'up -d' in all dependencies (in order of appearance) and then in the actual app. 73 | 74 | You can also do: 75 | 76 | ``` 77 | docker-app stop 78 | ``` 79 | 80 | By default, this command only stops the containers from the docker-compose.yml file in the actual application. If you want to stop the actual app with all the containers in its dependencies, run: 81 | 82 | ``` 83 | docker-app stop --all 84 | ``` 85 | 86 | The same is valid for the restart command: 87 | 88 | ``` 89 | docker-app restart # or restart --all for all the dependencies 90 | ``` 91 | 92 | ### Executing stuff on containers 93 | 94 | You can use the *exec* command of docker-app: 95 | 96 | ``` 97 | docker-app exec command 98 | docker-app exec command -c another_container # run the command in another container 99 | ``` 100 | 101 | ### Launching a bash console in a container 102 | 103 | **Without docker-app** 104 | 105 | ``` 106 | docker-compose exec container_name bash 107 | ``` 108 | 109 | **With docker-app** 110 | 111 | ``` 112 | docker-app bash 113 | ``` 114 | 115 | The last example is going to assume that the container to use has the same name that the actual directory where you executed docker-app. If you want to specify a specific container, you can use the *-c* (o **--container**) flag: 116 | 117 | ``` 118 | docker-app bash -c other_container_in_docker_compose 119 | ``` 120 | 121 | NOTE: if you want to launch a command using exec bash (as in the original docker-compose) you need to use the exec wrapper of docker-app (because the -c flag on bash is going to interfere with the -c flag on docker-app): 122 | 123 | ``` 124 | docker-app exec bash "-c bundle exec rails console" 125 | docker-app exec bash "-c bundle exec rails console" -c another_container 126 | ``` 127 | 128 | ### Ruby/Rails integration 129 | 130 | docker-app detects if the current app has a Gemfile and allows for a more concise syntax when using bundler or rails: 131 | 132 | **Without docker-app** 133 | 134 | ``` 135 | docker-compose exec container_name bundle install 136 | ``` 137 | 138 | **With docker-app** 139 | 140 | ``` 141 | docker-app bundle install 142 | ``` 143 | 144 | **Without docker-app** 145 | 146 | ``` 147 | docker-compose exec container_name bundle exec rails console 148 | ``` 149 | 150 | **With docker-app** 151 | 152 | ``` 153 | docker-app rails console 154 | ``` 155 | 156 | Note that every rails command is executed using *bundle exec*. 157 | 158 | The *-c* (*--console*) flag is available to the bundle/rails shortcut also: 159 | 160 | ``` 161 | docker-app rails console -c another_container 162 | ``` 163 | 164 | # TODO 165 | 166 | - test! 167 | -------------------------------------------------------------------------------- /bin/docker-app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | docker-app v0.1.2 5 | author: diego.acuna@mailbox.org 6 | 7 | docker-app allows to define external docker-compose dependencies 8 | on a docker-compose app. Also, detects (for now) ruby/rails environments 9 | so an user can write rails command in a container using a more concise syntax. 10 | """ 11 | from contextlib import contextmanager 12 | from argparse import RawTextHelpFormatter 13 | import os 14 | import sys 15 | import yaml 16 | import logging 17 | import argparse 18 | import subprocess 19 | 20 | def execute_cmd(cmd, options=None): 21 | params_list = [cmd] if options is None else [cmd] + options 22 | p = subprocess.Popen(params_list, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 23 | output, error = p.communicate() 24 | return output, error, p.returncode 25 | 26 | @contextmanager 27 | def cd(newdir): 28 | prevdir = os.getcwd() 29 | os.chdir(os.path.expanduser(newdir)) 30 | try: 31 | yield 32 | finally: 33 | os.chdir(prevdir) 34 | 35 | def execute_in_containers(cmd_params, all_containers=True): 36 | if all_containers: 37 | if 'dependencies' in parsed: 38 | for service in parsed['dependencies']: 39 | dwd = pwd.replace(dc_name, service) 40 | with cd(dwd): 41 | output, error, rcode = execute_cmd('docker-compose', cmd_params) 42 | if rcode == 0: 43 | logging.info("ON SERVICE {0}:\n{1}\n{2}".format(service, output, error)) 44 | else: 45 | logging.info(error) 46 | sys.exit(-1) 47 | # now we execute the command in the current dir 48 | return execute_cmd('docker-compose', cmd_params) 49 | 50 | # cons values 51 | CONF_FILE = 'docker-app.yml' 52 | UP_CMD = ['up', '-d'] 53 | STOP_CMD = ['stop'] 54 | RESTART_CMD = ['restart'] 55 | 56 | if __name__ == "__main__": 57 | logging.basicConfig(level=logging.INFO) 58 | # valid arguments for command line parsing 59 | parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, epilog="Available Actions:\n" + \ 60 | "- up: start the container with all their dependencies (in background mode, no need to specify --all)\n" + \ 61 | "- stop: stop the container (with all their dependencies if --all is specified)\n" + \ 62 | "- restart: restart the container (with all their dependencies if --all is specified)\n" + \ 63 | "- rails: if the software detects a rails app, execute the given rails command\n" + \ 64 | "- bash: start the bash console in the main container") 65 | parser.add_argument("action", help="Action to execute") 66 | parser.add_argument('command', nargs='?', default='console', help="Aditional command to execute (for example using the rails action)") 67 | parser.add_argument('-a', '--all', action='store_true', help='Apply the action to all containers (including dependencies)') 68 | parser.add_argument('-c', '--container', help='Container where the action is going to be executed (for actions like bash or rails). ' + \ 69 | 'If it is not specified, then the main container (from config file or directory name) is used.') 70 | 71 | # by default we want to show argparse help messages 72 | if len(sys.argv)==1: 73 | parser.print_help() 74 | sys.exit(1) 75 | # get cmd arguments 76 | args = parser.parse_args() 77 | 78 | pwd = os.getcwd() 79 | # this is the name of the actual service, if no main is specified in the config 80 | # file, then we assume that this is the main container in the docker-compose file 81 | dc_name = pwd.split("/")[-1] 82 | 83 | parsed = {} # by default we have no properties 84 | # we look for a docker-app.yml file 85 | if os.path.isfile(CONF_FILE): 86 | with open(CONF_FILE) as f: 87 | content = f.read() 88 | try: 89 | parsed = yaml.load(content) 90 | except yaml.scanner.ScannerError as e: 91 | logging.error("ERROR: {0}".format(e).format(e)) 92 | # if we have a main property, then that is the main container in the docker-compose file 93 | main_container = parsed['main'] if 'main' in parsed else dc_name 94 | # proccess the current action 95 | if args.action == 'up': 96 | output, error, rcode = execute_in_containers(UP_CMD) 97 | logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error)) 98 | elif args.action == 'stop': 99 | output, error, rcode = execute_in_containers(STOP_CMD, all_containers=args.all) 100 | logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error)) 101 | elif args.action == 'restart': 102 | output, error, rcode = execute_in_containers(RESTART_CMD, all_containers=args.all) 103 | logging.info("ON SERVICE {0}:\n{1}\n{2}".format(dc_name, output, error)) 104 | elif args.action == 'bash': 105 | if args.container: 106 | main_container = args.container 107 | sys.exit(subprocess.call(['docker-compose', 'exec', main_container, 'bash'])) 108 | elif args.action in ['bundle', 'rails']: 109 | # first we check for a Gemfile 110 | if os.path.isfile('Gemfile'): 111 | logging.debug("Found a bundler configuration!") 112 | if args.container: 113 | main_container = args.container 114 | base_cmd = ['docker-compose', 'exec', main_container] 115 | if args.action == 'bundle': 116 | params = base_cmd + [args.action] if args.command is None else base_cmd + [args.action, args.command] 117 | logging.info("EXECUTING: " + " ".join(params)) 118 | sys.exit(subprocess.call(params)) 119 | if args.action == 'rails': 120 | params = base_cmd + ['bundle', 'exec', args.action] if args.command is None else base_cmd + ['bundle', 'exec', args.action, args.command] 121 | logging.info("EXECUTING: " + " ".join(params)) 122 | sys.exit(subprocess.call(params)) 123 | else: 124 | logging.info("No Gemfile in the actual directory.") 125 | elif args.action == 'exec': 126 | # this is the general exec command, its only a wrapper to the original exec in docker-compose 127 | if not args.command: 128 | logging.error("ERROR: You need to specify a command to execute") 129 | else: 130 | if args.container: 131 | main_container = args.container 132 | params = ['docker-compose', 'exec', main_container, args.command] 133 | logging.info("EXECUTING: " + " ".join(params)) 134 | sys.exit(subprocess.call(params)) 135 | else: 136 | logging.error("Command not found!") 137 | 138 | --------------------------------------------------------------------------------