├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker_replay ├── __init__.py ├── args.py ├── models.py ├── opts.py ├── parser.py └── version.py └── setup.py /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Run docker-replay inside Docker 2 | # 3 | # Example: 4 | # docker build -t replay . 5 | # docker run -v /var/run/docker.sock:/var/run/docker.sock replay -p nginx 6 | 7 | FROM quay.io/vektorcloud/python:3 8 | 9 | WORKDIR /usr/src/app 10 | COPY . . 11 | RUN pip install . 12 | 13 | ENTRYPOINT ["docker-replay"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 bradley 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-replay 2 | 3 | [![PyPI version](https://badge.fury.io/py/docker-replay.svg)](https://badge.fury.io/py/docker-replay) 4 | 5 | Generate `docker run` command and options from running containers 6 | 7 | ## Quickstart 8 | 9 | `docker-replay` can be most easily run using the official image build: 10 | ```bash 11 | docker run --rm -ti \ 12 | -v /var/run/docker.sock:/var/run/docker.sock \ 13 | bcicen/docker-replay \ 14 | -p 15 | ``` 16 | 17 | ## Installing 18 | 19 | ```bash 20 | pip install docker-replay 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```bash 26 | docker-replay -p 27 | ``` 28 | 29 | output: 30 | ```bash 31 | docker run --env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ 32 | --hostname test \ 33 | --interactive \ 34 | --tty \ 35 | --add-host google.com:127.0.0.1 \ 36 | --memory 128m \ 37 | --memory-swap 256m \ 38 | --memory-swappiness -1 \ 39 | --name test \ 40 | --expose 80/tcp \ 41 | --restart on-failure:0 \ 42 | --entrypoint "echo" \ 43 | alpine:latest \ 44 | hello 45 | ``` 46 | 47 | ## Options 48 | 49 | Option | Description 50 | --- | --- 51 | --debug, -d | enable debug output 52 | --pretty-print, -p | pretty-print output 53 | -------------------------------------------------------------------------------- /docker_replay/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | from argparse import ArgumentParser 5 | 6 | from docker_replay.version import version 7 | from docker_replay.opts import config_disables 8 | 9 | log = logging.getLogger('docker-replay') 10 | 11 | class DockerReplay(object): 12 | def __init__(self, container_id, pretty_print=True): 13 | from docker import client, errors 14 | from docker_replay.parser import ConfigParser 15 | 16 | self.pretty_print = pretty_print 17 | 18 | c = client.from_env(version='auto') 19 | 20 | try: 21 | inspect = c.api.inspect_container(container_id) 22 | self.parser = ConfigParser(inspect) 23 | except errors.NotFound: 24 | print('no such container: %s' % container_id) 25 | sys.exit(1) 26 | 27 | def __str__(self): 28 | # remove conflicting options 29 | drop_opts = [] 30 | for o in self.parser.opts: 31 | if o.name in config_disables: 32 | drop_opts += config_disables[o.name] 33 | 34 | output = sorted([ str(o) for o in self.parser.opts \ 35 | if o.name not in drop_opts ]) 36 | output += [ str(a) for a in self.parser.args ] 37 | 38 | if self.pretty_print: 39 | return 'docker run %s' % ' \\\n '.join(output) 40 | return 'docker run %s' % ' '.join(output) 41 | 42 | def main(): 43 | argparser = ArgumentParser(description='docker-replay v%s' % version) 44 | argparser.add_argument('-d', '--debug', action='store_true', 45 | help='enable debug output') 46 | argparser.add_argument('-p', '--pretty-print', action='store_true', 47 | help='pretty-print output') 48 | argparser.add_argument('container', 49 | help='container to generate command from') 50 | args = argparser.parse_args() 51 | 52 | if args.debug: 53 | logging.basicConfig(level=logging.DEBUG) 54 | else: 55 | logging.basicConfig(level=logging.WARN) 56 | 57 | print(DockerReplay(args.container, args.pretty_print)) 58 | -------------------------------------------------------------------------------- /docker_replay/args.py: -------------------------------------------------------------------------------- 1 | from docker_replay.models import DockerArg 2 | 3 | class ArgParser(object): 4 | def __init__(self, o_name, o_key): 5 | self.name = o_name 6 | self.key = o_key 7 | 8 | def build(self, val): 9 | yield DockerArg(self.name, val) 10 | 11 | class CmdParser(ArgParser): 12 | def build(self, val): 13 | if val: 14 | val = ' '.join(val) 15 | yield DockerArg(self.name, val) 16 | 17 | config_args = [ 18 | ArgParser('image', 'Config.Image'), 19 | CmdParser('cmd', 'Config.Cmd'), 20 | ] 21 | -------------------------------------------------------------------------------- /docker_replay/models.py: -------------------------------------------------------------------------------- 1 | 2 | class DockerParam(object): 3 | """ Base class for options or arguments """ 4 | 5 | default = None 6 | 7 | def __init__(self, name, val): 8 | self.name = name 9 | self.value = val 10 | 11 | def is_null(self): 12 | if self.value: 13 | return False 14 | return True 15 | 16 | class DockerArg(DockerParam): 17 | """ Represents a positional argument to `docker run` """ 18 | 19 | def __str__(self): 20 | return self.value 21 | 22 | class DockerOpt(DockerParam): 23 | """ Represents an option to `docker run` """ 24 | 25 | def __str__(self): 26 | return '%s %s' % (self.name, self.value) 27 | 28 | """ 29 | Generic option types 30 | """ 31 | 32 | class BoolOpt(DockerOpt): 33 | def __init__(self, *args): 34 | super(DockerOpt, self).__init__(*args) 35 | # DockerOpt.__init__(self, *args) 36 | 37 | def __str__(self): 38 | return self.name 39 | 40 | class ByteValueOpt(DockerOpt): 41 | """ Option with one or more user-defined values """ 42 | 43 | @staticmethod 44 | def format_bytes(x): 45 | KB = 1024 46 | MB = KB*1024 47 | GB = MB*1024 48 | 49 | def _round(x): 50 | return int(round(x)) 51 | 52 | x = float(x) 53 | if x < 1024: 54 | return '%sb' % _round(x) 55 | elif 1024 <= x < MB: 56 | return '%sk' % _round(x/KB) 57 | elif KB <= x < GB: 58 | return '%sm' % _round(x/MB) 59 | elif GB <= x: 60 | return '%sg' % _round(x/GB) 61 | 62 | def __str__(self): 63 | try: 64 | return '%s %s' % (self.name, self.format_bytes(self.value)) 65 | except ValueError: 66 | raise TypeError('unsupported value type for option "%s": %s' % \ 67 | (self.name, self.value)) 68 | 69 | class ValueOpt(DockerOpt): 70 | """ Option with one or more user-defined values """ 71 | 72 | def __str__(self): 73 | try: 74 | return '%s %s' % (self.name, self.value) 75 | except ValueError: 76 | raise TypeError('unsupported value type for option "%s": %s' % \ 77 | (self.name, self.value)) 78 | 79 | class MapOpt(ValueOpt): 80 | """ Option with one or more user-defined mappings """ 81 | 82 | default = {} 83 | -------------------------------------------------------------------------------- /docker_replay/opts.py: -------------------------------------------------------------------------------- 1 | #TODO: ulimits 2 | 3 | from docker_replay.models import * 4 | 5 | class OptParser(object): 6 | def __init__(self, o_name, o_key, o_type): 7 | self.name = o_name 8 | self.key = o_key 9 | self.otype = o_type 10 | 11 | # parse given value and yield zero or more DockerOpts 12 | def build(self, val): 13 | # yield a new opt for multi-value options(--volume, --env, etc.) 14 | if self.otype == ValueOpt and isinstance(val, list): 15 | for v in val: 16 | yield self.build_one(v) 17 | elif self.otype == MapOpt and isinstance(val, dict): 18 | for k,v in val.items(): 19 | val = '%s=%s' % (k,v) 20 | yield self.build_one(val) 21 | else: 22 | yield self.build_one(val) 23 | 24 | def build_one(self, val): 25 | return self.otype(self.name, val) 26 | 27 | class NameParser(OptParser): 28 | def build(self, val): 29 | yield self.build_one(val.strip('/')) 30 | 31 | class PublishedParser(OptParser): 32 | def build(self, val): 33 | if not val: 34 | return 35 | 36 | def read_hostports(hplist): 37 | for hp in hplist: 38 | if hp['HostIp']: 39 | yield '%s:%s' % (hp['HostIp'], hp['HostPort']) 40 | else: 41 | yield hp['HostPort'] 42 | 43 | for cport, binds in val.items(): 44 | cport, proto = cport.split('/') 45 | for hostport in read_hostports(binds): 46 | v = '%s:%s/%s' % (hostport,cport,proto) 47 | yield self.build_one(v) 48 | 49 | class ExposedParser(OptParser): 50 | def build(self, val): 51 | if not val: 52 | return 53 | for port in val.keys(): 54 | yield self.build_one(port) 55 | 56 | class LinkParser(OptParser): 57 | def build(self, val): 58 | if not val: 59 | return 60 | for link in val: 61 | src, linkname = link.split(':') 62 | v = '%s:%s' % (src.strip('/'), linkname.split('/')[-1]) 63 | yield self.build_one(v) 64 | 65 | class NetModeParser(OptParser): 66 | def build(self, val): 67 | if val == "default": 68 | return 69 | yield self.build_one(val) 70 | 71 | class RestartParser(OptParser): 72 | def build(self, val): 73 | if val['Name'] == 'no': 74 | return 75 | v = val['Name'] 76 | if val['MaximumRetryCount'] != 0: 77 | v += ':%s' % val['MaximumRetryCount'] 78 | yield self.build_one(v) 79 | 80 | class EntrypointParser(OptParser): 81 | def build(self, val): 82 | if val: 83 | v = '"%s"' % ' '.join(val) 84 | yield self.build_one(v) 85 | 86 | config_opts = [ 87 | OptParser('--env', 'Config.Env', ValueOpt), 88 | OptParser('--hostname', 'Config.Hostname', ValueOpt), 89 | OptParser('--interactive', 'Config.OpenStdin', BoolOpt), 90 | OptParser('--label', 'Config.Labels', MapOpt), 91 | OptParser('--tty', 'Config.Tty', BoolOpt), 92 | OptParser('--user', 'Config.User', ValueOpt), 93 | OptParser('--workdir', 'Config.WorkingDir', ValueOpt), 94 | OptParser('--add-host', 'HostConfig.ExtraHosts', ValueOpt), 95 | OptParser('--blkio-weight', 'HostConfig.BlkioWeight', ValueOpt), 96 | OptParser('--blkio-weight-device', 'HostConfig.BlkioWeightDevice', ValueOpt), 97 | OptParser('--cap-add', 'HostConfig.CapAdd', ValueOpt), 98 | OptParser('--cap-drop', 'HostConfig.CapDrop', ValueOpt), 99 | OptParser('--cgroup-parent', 'HostConfig.CgroupParent', ValueOpt), 100 | OptParser('--cidfile', 'HostConfig.ContainerIDFile', ValueOpt), 101 | OptParser('--cpu-period', 'HostConfig.CpuPeriod', ValueOpt), 102 | OptParser('--cpu-shares', 'HostConfig.CpuShares', ValueOpt), 103 | OptParser('--cpu-quota', 'HostConfig.CpuQuota', ValueOpt), 104 | OptParser('--cpuset-cpus', 'HostConfig.CpusetCpus', ValueOpt), 105 | OptParser('--cpuset-mems', 'HostConfig.CpusetMems', ValueOpt), 106 | OptParser('--device', 'HostConfig.Devices', ValueOpt), 107 | OptParser('--device-read-bps', 'HostConfig.BlkioDeviceReadBps', ValueOpt), 108 | OptParser('--device-read-iops', 'HostConfig.BlkioDeviceReadIOps', ValueOpt), 109 | OptParser('--device-write-bps', 'HostConfig.BlkioDeviceWriteBps', ValueOpt), 110 | OptParser('--device-write-iops', 'HostConfig.BlkioDeviceWriteIOps', ValueOpt), 111 | OptParser('--dns', 'HostConfig.Dns', ValueOpt), 112 | OptParser('--dns-opt', 'HostConfig.DnsOptions', ValueOpt), 113 | OptParser('--dns-search', 'HostConfig.DnsSearch', ValueOpt), 114 | OptParser('--group-add', 'HostConfig.GroupAdd', ValueOpt), 115 | OptParser('--ipc', 'HostConfig.IpcMode', ValueOpt), 116 | OptParser('--isolation', 'HostConfig.Isolation', ValueOpt), 117 | OptParser('--kernel-memory', 'HostConfig.KernelMemory', ValueOpt), 118 | OptParser('--log-driver', 'HostConfig.LogConfig.Type', ValueOpt), 119 | OptParser('--log-opt', 'HostConfig.LogConfig.Config', MapOpt), 120 | OptParser('--memory', 'HostConfig.Memory', ByteValueOpt), 121 | OptParser('--memory-reservation', 'HostConfig.MemoryReservation', ByteValueOpt), 122 | OptParser('--memory-swap', 'HostConfig.MemorySwap', ByteValueOpt), 123 | OptParser('--memory-swappiness', 'HostConfig.MemorySwappiness', ValueOpt), 124 | OptParser('--oom-kill-disable', 'HostConfig.OomKillDisable', BoolOpt), 125 | OptParser('--oom-score-adj', 'HostConfig.OomScoreAdj', ValueOpt), 126 | OptParser('--publish-all', 'HostConfig.PublishAllPorts', BoolOpt), 127 | OptParser('--pid', 'HostConfig.PidMode', ValueOpt), 128 | OptParser('--pids-limit', 'HostConfig.PidsLimit', ValueOpt), 129 | OptParser('--privileged', 'HostConfig.Privileged', BoolOpt), 130 | OptParser('--read-only', 'HostConfig.ReadonlyRootfs', BoolOpt), 131 | OptParser('--rm', 'HostConfig.AutoRemove', BoolOpt), 132 | OptParser('--volume', 'HostConfig.Binds', ValueOpt), 133 | OptParser('--security-opt', 'HostConfig.SecurityOpt', ValueOpt), 134 | OptParser('--shm-size', 'HostConfig.ShmSize', ByteValueOpt), 135 | OptParser('--userns', 'HostConfig.UsernsMode', ValueOpt), 136 | OptParser('--uts', 'HostConfig.UTSMode', ValueOpt), 137 | OptParser('--volume-driver', 'HostConfig.VolumeDriver', ValueOpt), 138 | OptParser('--volumes-from', 'HostConfig.VolumesFrom', ValueOpt), 139 | OptParser('--ip', 'NetworkSettings.IPAddress', ValueOpt), 140 | # OptParser('--ip6', 'LinkLocalIPv6Address', ValueOpt), 141 | # OptParser('--netdefault', '???', ValueOpt), 142 | # OptParser('--net-alias', '???', ValueOpt), 143 | OptParser('--mac-address', 'NetworkSettings.MacAddress', ValueOpt), 144 | 145 | # non-generic opt parsers 146 | EntrypointParser('--entrypoint', 'Config.Entrypoint', DockerOpt), 147 | ExposedParser('--expose', 'Config.ExposedPorts', DockerOpt), 148 | LinkParser('--link', 'HostConfig.Links', DockerOpt), 149 | NameParser('--name', 'Name', DockerOpt), 150 | NetModeParser('--net', 'HostConfig.NetworkMode', DockerOpt), 151 | PublishedParser('--publish', 'HostConfig.PortBindings', DockerOpt), 152 | RestartParser('--restart', 'HostConfig.RestartPolicy', DockerOpt), 153 | ] 154 | 155 | # options which, if defined, will omit the value options from output 156 | config_disables = { 157 | '--uts': ['--hostname'], 158 | } 159 | -------------------------------------------------------------------------------- /docker_replay/parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from docker_replay.args import config_args 3 | from docker_replay.opts import config_opts 4 | from docker_replay.models import DockerOpt, DockerArg 5 | 6 | log = logging.getLogger('docker-replay') 7 | 8 | class ConfigParser(object): 9 | 10 | args = [] 11 | opts = [] 12 | 13 | def __init__(self, config): 14 | self.config = config 15 | 16 | # build all options 17 | for op in config_opts: 18 | o_val = self.get(op.key, op.otype.default) 19 | self.opts += list(op.build(o_val)) 20 | 21 | for ap in config_args: 22 | o_val = self.get(ap.key) 23 | self.args += list(ap.build(o_val)) 24 | 25 | olen, alen = len(self.opts), len(self.args) 26 | 27 | # filter null opts 28 | self.opts = [ o for o in self.opts if not o.is_null() ] 29 | self.args = [ o for o in self.args if not o.is_null() ] 30 | 31 | log.info('parsed %d options (%d configured)' % (olen, len(self.opts))) 32 | log.info('parsed %d args (%d configured)' % (alen, len(self.args))) 33 | 34 | def get(self, key, default=None): 35 | """ 36 | Retrieve a top-level or nested key, e.g: 37 | >>> get('Id') 38 | >>> get('HostConfig.Binds') 39 | """ 40 | key_parts = key.split('.') 41 | config = self.config 42 | while key_parts: 43 | try: 44 | config = config[key_parts.pop(0)] 45 | except KeyError: 46 | log.warn('returning default for missing key: %s' % key) 47 | return default 48 | log.debug('get key: %s (%s)' % (key, config)) 49 | return config 50 | -------------------------------------------------------------------------------- /docker_replay/version.py: -------------------------------------------------------------------------------- 1 | __version__ = (1, 5) 2 | version = '%d.%d' % __version__ 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | exec(open('docker_replay/version.py').read()) 4 | 5 | setup(name='docker-replay', 6 | version=version, 7 | packages=['docker_replay'], 8 | description='Generate docker run commands from running containers', 9 | author='Bradley Cicenas', 10 | author_email='bradley.cicenas@gmail.com', 11 | url='https://github.com/bcicen/docker-replay', 12 | install_requires=['docker>=2.4.2'], 13 | license='http://opensource.org/licenses/MIT', 14 | classifiers=( 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: MIT License ', 17 | 'Natural Language :: English', 18 | 'Programming Language :: Python', 19 | 'Programming Language :: Python :: 2.7', 20 | 'Programming Language :: Python :: 3.4', 21 | ), 22 | keywords='docker docker-py devops', 23 | entry_points = { 24 | 'console_scripts' : ['docker-replay = docker_replay:main'] 25 | } 26 | ) 27 | --------------------------------------------------------------------------------