├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── dokku_client ├── __init__.py ├── client.py ├── command_tools.py └── commands │ ├── __init__.py │ ├── config.py │ ├── help.py │ ├── prompt.py │ └── restart.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .DS_Store 4 | /dist -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Change-log for dokku-client. 2 | 3 | This file will be added to as part of each release 4 | 5 | ---- 6 | 7 | Version 0.2.3, Wed 21 May 2014 8 | =============================== 9 | 10 | 9bbd06be4c Install sarge as dependency. (Adam) 11 | 00602db1ff Adding badges to readme (Adam Charnock) 12 | 4269c36f9a updating commands in readme (Adam Charnock) 13 | 14 | 15 | Version 0.2.2, Mon 05 Aug 2013 16 | =============================== 17 | 18 | eab285dd72 Adding restart command (Adam Charnock) 19 | 20 | 21 | Version 0.2.1, Mon 05 Aug 2013 22 | =============================== 23 | 24 | 7aee95dd9f Displaying commands in a more rational order (Adam Charnock) 25 | 26 | 27 | Version 0.2.0, Mon 05 Aug 2013 28 | =============================== 29 | 30 | 6193973edb Adding config (environment) management commands (Adam Charnock) 31 | d8cccbdf5e Slight refactoring and adding utilities to BaseCommand (Adam Charnock) 32 | f9d87ab810 Adding link to seed in readme (Adam Charnock) 33 | 2a1fafdd3e updating readme for latest seed changes (Adam Charnock) 34 | bdf991d3c3 Adding link to Readme to Dokku (Adam Charnock) 35 | 698d616153 readme updates (Adam Charnock) 36 | 35a51783e7 adding docs on command creation (Adam Charnock) 37 | 8340a4a7fa adding docs on command creation (Adam Charnock) 38 | 945baa983a Adding content to readme (Adam Charnock) 39 | 422b7d6b18 Adding /dist to gitignore (Adam Charnock) 40 | 41 | 42 | Version 0.1.0 (first version), Sun 04 Aug 2013 43 | =============================================== 44 | 45 | 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Adam Charnock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Heroku-like command line interface for `Dokku`_ 2 | =============================================== 3 | 4 | **Note:** This project is in the very early stages of development. 5 | You can help by adding commands (see below). 6 | 7 | .. image:: https://badge.fury.io/py/dokku-client.png 8 | :target: https://badge.fury.io/py/dokku-client 9 | 10 | .. image:: https://pypip.in/d/dokku-client/badge.png 11 | :target: https://pypi.python.org/pypi/dokku-client 12 | 13 | Installation 14 | ------------ 15 | 16 | .. code-block:: bash 17 | 18 | pip install dokku-client 19 | 20 | Configuration 21 | ------------- 22 | 23 | You can specify the Dokku host & app on the command line, but you may 24 | find it convenient to set the following environment variables instead: 25 | 26 | .. code-block:: bash 27 | 28 | export DOKKU_HOST=ubuntu@myserver.com 29 | export DOKKU_APP=my-app-name 30 | 31 | Setting these variables in your virtualenv's `postactivate` hook may 32 | be useful. 33 | 34 | Usage 35 | ----- 36 | 37 | Once installed, usage is simple: 38 | 39 | .. code-block:: bash 40 | 41 | dokku-client help 42 | 43 | Produces: 44 | 45 | .. code-block:: none 46 | 47 | Client for Dokku 48 | 49 | usage: 50 | dokku-client [...] 51 | dokku-client help 52 | 53 | global options: 54 | -H , --host= Host address 55 | -a , --app= App name 56 | 57 | full list of available commands: 58 | 59 | help Show this help message 60 | configget Set one or more config options 61 | configset Set one or more config options in the app's ENV file 62 | prompt Open a prompt 63 | restart Restart the container 64 | 65 | See 'git help ' for more information on a specific command. 66 | 67 | Contributing new commands 68 | ------------------------- 69 | 70 | Dokku-client allows any developer to hook in extra commands. This is done using 71 | exactly the same mechanism that dokku-client uses internally, that of entry points 72 | provided by ``setuptools``. 73 | 74 | First, create a python package. You may have your own favorite way of doing this, but I 75 | use seed_: 76 | 77 | .. code-block:: bash 78 | 79 | mkdir dokku-client-mycommand 80 | cd dokku-client-mycommand 81 | pip install seed 82 | seed create 83 | ls 84 | 85 | Second, create a class which extends ``dokku_client.BaseCommand`` and implements the method 86 | ``main(args)``. Also, the doc-block at the top 87 | of the class will be used by docopt_ to parse any command line arguments, so make 88 | sure you include that. See the `prompt command`_ for an example. 89 | 90 | And third, in your new ``setup.py`` file, specify your new class as an entry point: 91 | 92 | .. code-block:: python 93 | 94 | entry_points={ 95 | 'dokku_client.commands': [ 96 | 'mycommand = dokku_client_mycommand.mycommand:MyCommand', 97 | ], 98 | } 99 | 100 | Run ``setup.py`` so that the new entry point is initialized: 101 | 102 | .. code-block:: bash 103 | 104 | # Run in develop mode, so files will not be copied away. 105 | # You can continue to edit your code as usual 106 | python setup.py develop 107 | 108 | You should now find that your new command is available in dokku-client, 109 | run ``dokku-client help`` to check. 110 | 111 | Once done, you can release your package to PyPi using ``seed release --initial``. 112 | 113 | .. _Dokku: https://github.com/progrium/dokku 114 | .. _docopt: http://docopt.org/ 115 | .. _prompt command: https://github.com/adamcharnock/dokku-client/blob/master/dokku_client/commands/prompt.py 116 | .. _seed: https://github.com/adamcharnock/seed 117 | -------------------------------------------------------------------------------- /dokku_client/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # It must be possible to import this file with 3 | # none of the package's dependencies installed 4 | 5 | __version__ = "0.2.3" 6 | -------------------------------------------------------------------------------- /dokku_client/client.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """Client for Dokku 3 | 4 | usage: 5 | dokku-client [...] 6 | dokku-client help 7 | 8 | global options: 9 | -H , --host= Host address 10 | -a , --app= App name 11 | 12 | ---- 13 | 14 | See 'git help ' for more information on a specific command. 15 | """ 16 | import sys 17 | from subprocess import call 18 | 19 | from docopt import docopt 20 | 21 | from dokku_client import __version__ 22 | from dokku_client.command_tools import load_commands, command_by_name, apply_defaults, root_doc 23 | 24 | 25 | def main(): 26 | commands = load_commands() 27 | 28 | args = docopt(root_doc(), version='dokku-client version %s' % __version__, options_first=True) 29 | command_name = args[''] or 'help' 30 | 31 | if command_name == 'version': 32 | # docopt will handle version printing for us if use '--version' 33 | exit(call(['dokku-client', '--version'])) 34 | 35 | # Get the command object for the specified command name 36 | command = command_by_name(command_name, commands) 37 | if not command: 38 | sys.stderr.write("Unknown command. Use 'dokku-client help' for list of commands.\n") 39 | exit(1) 40 | else: 41 | # Use docopt to parse the options based upon the class' doc string 42 | command_args = docopt(command.doc) 43 | # Load default values from the users' environment 44 | command_args = apply_defaults(command_args) 45 | if command.check_config: 46 | # Sanity check the config 47 | if not command_args.get('--host', None): 48 | sys.stderr.write("Could not determine host. Specify --host or set DOKKU_HOST.\n") 49 | exit(1) 50 | if not command_args.get('--app', None): 51 | sys.stderr.write("Could not determine app. Specify --app or set DOKKU_APP.\n") 52 | exit(1) 53 | # Ok, let's run the command 54 | command.args = command_args 55 | command.main() 56 | 57 | if __name__ == '__main__': 58 | main() -------------------------------------------------------------------------------- /dokku_client/command_tools.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import pkg_resources 4 | 5 | def load_commands(): 6 | commands = [] 7 | for entry_point in pkg_resources.iter_entry_points(group='dokku_client.commands'): 8 | command_class = entry_point.load() 9 | command = command_class(name=entry_point.name) 10 | commands.append(command) 11 | commands = sorted(commands, key=lambda c: (c.sort_order, c.name)) 12 | return commands 13 | 14 | def command_list_doc(commands=None): 15 | commands = commands or load_commands() 16 | 17 | doc = "full list of available commands:\n\n" 18 | names = [] 19 | descriptions = [] 20 | 21 | for command in commands: 22 | names.append(command.name) 23 | descriptions.append(command.description) 24 | 25 | name_col_length = max(len(n) for n in names) 26 | for name, description in zip(names, descriptions): 27 | doc += " %s %s\n" % (name.ljust(name_col_length), description) 28 | 29 | return doc.strip() 30 | 31 | def root_doc(commands=None): 32 | from dokku_client import client 33 | doc = client.__doc__.replace('----', command_list_doc()) 34 | return doc.strip() 35 | 36 | 37 | def command_by_name(name, commands=None): 38 | commands = commands or load_commands() 39 | for command in commands: 40 | if command.name == name: 41 | return command 42 | return None 43 | 44 | def global_opts_doc(): 45 | from dokku_client import client 46 | lines = client.__doc__.split("\n") 47 | lines = filter(lambda l: re.match(r'\s*-[a-zA-Z].*', l), lines) 48 | return "\n".join(lines) 49 | 50 | def apply_defaults(args): 51 | for key, val in args.items(): 52 | if not val: 53 | env_var = 'DOKKU_%s' % key.replace('--', '').upper() 54 | if os.environ.get(env_var): 55 | args[key] = os.environ.get(env_var).strip() 56 | return args 57 | 58 | 59 | -------------------------------------------------------------------------------- /dokku_client/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from sarge import capture_stdout, run 4 | 5 | from dokku_client.command_tools import global_opts_doc 6 | 7 | class BaseCommand(object): 8 | check_config = True 9 | sort_order = 10 10 | 11 | def __init__(self, name): 12 | self._name = name 13 | # Set externally in client.py 14 | self.args = {} 15 | 16 | @property 17 | def doc(self): 18 | """Get the docs for this command 19 | 20 | The docs are used for display purposes, but also by 21 | docopt (which uses them to the parse command line options) 22 | """ 23 | 24 | doc = self.__doc__ 25 | # Unindent by one level 26 | doc = re.sub(r'^( {4}|\t)', '', doc, flags=re.MULTILINE) 27 | # Put the global options in place 28 | doc = re.sub(r'\s+\[\[include global options\]\]', "\n\n%s" % global_opts_doc(), doc) 29 | return doc 30 | 31 | @property 32 | def description(self): 33 | return self.__doc__.strip().split("\n")[0] 34 | 35 | @property 36 | def name(self): 37 | return self._name 38 | 39 | def main(self, global_args, command_args): 40 | raise NotImplementedError('Implement the main() method to implement your command') 41 | 42 | def run_remote(self, cmd, input=None): 43 | host = self.args['--host'] 44 | return capture_stdout('ssh %s -- %s' % (host, cmd), input=input) 45 | 46 | def restart_container(self): 47 | app = self.args['--app'] 48 | self.run_remote('docker restart `cat /home/git/%s/CONTAINER`' % app) 49 | -------------------------------------------------------------------------------- /dokku_client/commands/config.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from dokku_client.commands import BaseCommand 4 | 5 | class ConfigSetCommand(BaseCommand): 6 | """Set one or more config options in the app's ENV file 7 | 8 | usage: 9 | dokku-client configset [options] (=)... 10 | 11 | global options: 12 | [[include global options]] 13 | """ 14 | 15 | def main(self): 16 | key_values = dict([kv.split('=') for kv in self.args['=']]) 17 | app = self.args['--app'] 18 | 19 | self.run_remote('sudo -u git touch /home/git/%s/ENV' % app) 20 | p = self.run_remote('sudo -u git cat /home/git/%s/ENV' % app) 21 | env = p.stdout.text.strip() 22 | lines = env.split("\n") 23 | # For each line in the form 'export KEY=...' 24 | for i, line in enumerate(lines): 25 | matches = re.match('^export (.*?)=', line) 26 | if matches: 27 | key = matches.group(1) 28 | # If the key has been specified, then replace the line 29 | if key in key_values: 30 | lines[i] = 'export %s="%s"' % (key, key_values[key]) 31 | del key_values[key] 32 | 33 | # Now append the remaining lnes 34 | for key, value in key_values.items(): 35 | lines.append('export %s=%s' % (key, value)) 36 | 37 | env = "\n".join(lines) 38 | self.run_remote('sudo -u git tee /home/git/python-django-sample/ENV', input=env) 39 | 40 | print "Config set, resting container" 41 | self.restart_container() 42 | 43 | 44 | class ConfigGetCommand(BaseCommand): 45 | """Set one or more config options 46 | 47 | usage: 48 | dokku-client configget [options] 49 | 50 | global options: 51 | [[include global options]] 52 | """ 53 | 54 | def main(self): 55 | app = self.args['--app'] 56 | p = self.run_remote('sudo -u git cat /home/git/%s/ENV' % app) 57 | env = p.stdout.text.strip() 58 | print env -------------------------------------------------------------------------------- /dokku_client/commands/help.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from dokku_client import client 4 | from dokku_client.commands import BaseCommand 5 | from dokku_client.command_tools import command_by_name, root_doc 6 | 7 | class HelpCommand(BaseCommand): 8 | """Show this help message 9 | 10 | usage: 11 | dokku-client help [COMMAND] 12 | """ 13 | check_config = False 14 | sort_order = 5 15 | 16 | def main(self): 17 | if not self.args['COMMAND']: 18 | print root_doc() 19 | else: 20 | command = command_by_name(self.args['COMMAND']) 21 | if not command: 22 | sys.stderr.write("Unknown command. Use 'dokku-client help' for list of commands.\n") 23 | else: 24 | print command.doc 25 | -------------------------------------------------------------------------------- /dokku_client/commands/prompt.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | 3 | from dokku_client.commands import BaseCommand 4 | 5 | class PromptCommand(BaseCommand): 6 | """Open a prompt 7 | 8 | usage: 9 | dokku-client prompt [options] 10 | 11 | global options: 12 | [[include global options]] 13 | """ 14 | 15 | def main(self): 16 | cmd = 'docker run -i -t app/%s /bin/bash' % self.args['--app'] 17 | call_args = ['ssh', '-t', self.args['--host'], cmd] 18 | print "Running command: %s" % ' '.join(call_args) 19 | call(call_args) 20 | -------------------------------------------------------------------------------- /dokku_client/commands/restart.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | 3 | from dokku_client.commands import BaseCommand 4 | 5 | class RestartCommand(BaseCommand): 6 | """Restart the container 7 | 8 | usage: 9 | dokku-client restart [options] 10 | 11 | global options: 12 | [[include global options]] 13 | """ 14 | 15 | def main(self): 16 | self.restart_container() 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os.path import exists 4 | from setuptools import setup, find_packages 5 | 6 | from dokku_client import __version__ 7 | 8 | setup( 9 | name='dokku-client', 10 | version=__version__, 11 | author='Adam Charnock', 12 | author_email='adam@adamcharnock.com', 13 | packages=find_packages(), 14 | scripts=[], 15 | url='https://github.com/adamcharnock/dokku-client', 16 | license='MIT', 17 | description='Heroku-style command line interface for Dokku', 18 | long_description=open('README.rst').read() if exists("README.rst") else "", 19 | install_requires=[ 20 | 'docopt>=0.6.1', 21 | 'sarge==0.1.1' 22 | ], 23 | entry_points={ 24 | 'dokku_client.commands': [ 25 | 'prompt = dokku_client.commands.prompt:PromptCommand', 26 | 'help = dokku_client.commands.help:HelpCommand', 27 | 'configset = dokku_client.commands.config:ConfigSetCommand', 28 | 'configget = dokku_client.commands.config:ConfigGetCommand', 29 | 'restart = dokku_client.commands.restart:RestartCommand', 30 | ], 31 | 'console_scripts': [ 32 | 'dokku-client = dokku_client.client:main', 33 | ] 34 | } 35 | ) 36 | --------------------------------------------------------------------------------