Utility to re-run a docker container with slightly different arguments
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

docker_rerun.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #!/usr/bin/python3
  2. """Re-runs a docker container using the same arguments as before.
  3. Given the name of a container, the previous arguments are determined
  4. and reconstructed by looking at the `docker inspect` output.
  5. Each function named `handle_*` handles one configuration option,
  6. reading the relevant information from the inspect output and adding
  7. the relevant command line flags to the config.
  8. Each function named `modify_*` allows the user to modify the
  9. configuration in some manner. Modify functions are called twice:
  10. once with an ArgumentParser, and subsequently with the parsed
  11. arguments and the container object.
  12. """
  13. import argparse
  14. import inspect
  15. import json
  16. import subprocess
  17. import sys
  18. class Container(object):
  19. """Encapsulates information about a container."""
  20. def __init__(self, container_info, image_info):
  21. """Creates a new Container.
  22. Args:
  23. name (str): The name of the container.
  24. container_info (dict): Dictionary describing the container state.
  25. image_info (dict): Dictionary describing the image state.
  26. """
  27. self.args = ['-d']
  28. """The arguments passed to docker to create the container."""
  29. self.image = ''
  30. """The image that the container uses."""
  31. self.cmd = []
  32. """The command executed within the container."""
  33. self.info = container_info
  34. """The container information as returned by `docker inspect`"""
  35. self.image_info = image_info
  36. """The image information as returned by `docker inspect`"""
  37. def command_line(self):
  38. """Gets the full command-line used to run this container."""
  39. return ['docker', 'run'] + sorted(self.args) + [self.image] + self.cmd
  40. def add_args_from_list(self, template, selector):
  41. """Adds an argument for each item in a list.
  42. Args:
  43. template (str): Template to use for the argument. Use %s for value.
  44. selector (func): Function to extract the list from our info.
  45. """
  46. target = selector(self.info)
  47. if target:
  48. self.args.extend([template % entry for entry in target])
  49. def if_image_diff(self, selector, fallback):
  50. """Gets a property if it's different from the image's config.
  51. Compares the value of a property in the container's information to the
  52. same property in the image information. If the value is different then
  53. the container's version is returned, otherwise the specified fallback
  54. is returned.
  55. This is useful where the container inherits config from the image,
  56. such as the command or the user to run as. We only want to include it
  57. in the arguments if it has been explicitly changed.
  58. Args:
  59. selector (func): Function to extract the property.
  60. fallback (object): Value to return if the properties are identical.
  61. """
  62. container = selector(self.info)
  63. image = selector(self.image_info)
  64. return fallback if container == image else container
  65. def docker_inspect(target, what):
  66. """Uses `docker inspect` to get details about the given container or image.
  67. Args:
  68. target (str): The name of the container or image to inspect.
  69. what (str): The type of object to inspect ('container' or 'image').
  70. Returns:
  71. dict: Detailed information about the target.
  72. Raises:
  73. CalledProcessError: An error occurred talking to Docker.
  74. """
  75. output = subprocess.check_output(['docker', 'inspect',
  76. '--type=%s' % what, target])
  77. return json.loads(output.decode('utf-8'))[0]
  78. def handle_binds(container):
  79. """Copies the volume bind (--volume/-v) arguments."""
  80. container.add_args_from_list('--volume=%s',
  81. lambda c: c['HostConfig']['Binds'])
  82. def handle_command(container):
  83. """Copies the command (trailing arguments)."""
  84. container.cmd = container.if_image_diff(lambda c: c['Config']['Cmd'], [])
  85. def handle_environment(container):
  86. """Copies the environment (-e/--env) arguments."""
  87. container_env = set(container.info['Config']['Env'] or [])
  88. image_env = set(container.image_info['Config']['Env'] or [])
  89. delta = container_env - image_env
  90. container.args.extend(['--env=%s' % env for env in delta])
  91. def handle_image(container):
  92. """Copies the image argument."""
  93. container.image = container.info['Config']['Image']
  94. def handle_labels(container):
  95. """Copies the label (-l/--label) arguments."""
  96. container_labels = set((container.info['Config']['Labels'] or {}).items())
  97. image_labels = set((container.image_info['Config']['Labels'] or {}).items())
  98. delta = container_labels - image_labels
  99. for key, value in delta:
  100. if value:
  101. container.args.append('--label=%s=%s' % (key, value))
  102. else:
  103. container.args.append('--label=%s' % key)
  104. def handle_links(container):
  105. """Copies the link (--link) arguments."""
  106. name = container.info['Name']
  107. links = container.info['HostConfig']['Links'] or []
  108. for link in links:
  109. (target, alias) = link.split(':')
  110. target = target[1:]
  111. alias = alias[len(name) + 1:]
  112. if alias == target:
  113. container.args.append('--link=%s' % target)
  114. else:
  115. container.args.append('--link=%s:%s' % (target, alias))
  116. def handle_name(container):
  117. """Copies the name (--name) argument."""
  118. # Trim the leading / off the name. They're equivalent from docker's point
  119. # of view, but having the plain name looks nicer from a human point of view.
  120. container.args.append('--name=%s' % container.info['Name'][1:])
  121. def handle_network_mode(container):
  122. """Copies the network mode (--net) argument."""
  123. network = container.info['HostConfig']['NetworkMode']
  124. if network != 'default':
  125. container.args.append('--network=%s' % network)
  126. def handle_ports(container):
  127. """Copies the port publication (-p) arguments."""
  128. ports = container.info['HostConfig']['PortBindings']
  129. if ports:
  130. for port, bindings in ports.items():
  131. # /tcp is the default - no need to include it
  132. port_name = port[:-4] if port.endswith('/tcp') else port
  133. for binding in bindings:
  134. if binding['HostIp']:
  135. container.args.append('-p=%s:%s:%s' % (binding['HostIp'],
  136. binding['HostPort'],
  137. port_name))
  138. elif binding['HostPort']:
  139. container.args.append('-p=%s:%s' % (binding['HostPort'],
  140. port_name))
  141. else:
  142. container.args.append('-p=%s' % port_name)
  143. def handle_restart(container):
  144. """Copies the restart policy (--restart) argument."""
  145. policy = container.info['HostConfig']['RestartPolicy']
  146. if policy and policy['Name'] != 'no':
  147. arg = '--restart=%s' % policy['Name']
  148. if policy['MaximumRetryCount'] > 0:
  149. arg += ':%s' % policy['MaximumRetryCount']
  150. container.args.append(arg)
  151. def handle_user(container):
  152. """Copies the user (--user/-u) argument."""
  153. user = container.if_image_diff(lambda c: c['Config']['User'], None)
  154. if user:
  155. container.args.append('--user=%s' % user)
  156. def handle_volumes_from(container):
  157. """Copies the volumes from (--volumes-from) argument."""
  158. container.add_args_from_list('--volumes-from=%s',
  159. lambda c: c['HostConfig']['VolumesFrom'])
  160. def modify_image(parser=None, args=None, container=None):
  161. """Allows the image (name, version, etc) to be modified in one go."""
  162. if parser:
  163. parser.add_argument('--image',
  164. help='Image to use in place of the original')
  165. elif args.image:
  166. container.image = args.image
  167. def modify_labels(parser=None, args=None, container=None):
  168. """Allows labels on the container to be modified."""
  169. if parser:
  170. parser.add_argument('--label', '-l', action='append',
  171. help='The new label to add to the container.')
  172. elif args.label:
  173. for label in args.label:
  174. # Remove any existing label with the same key
  175. prefix = label.split('=')[0]
  176. container.args = [arg for arg in container.args if
  177. not arg.startswith('--label=%s=' % prefix) and
  178. not arg == '--label=%s' % prefix]
  179. container.args.append('--label=%s' % label)
  180. def modify_network(parser=None, args=None, container=None):
  181. """Allows the network to be modified."""
  182. if parser:
  183. parser.add_argument('--network',
  184. help='The new network configuration to use')
  185. elif args.network:
  186. # Get rid of any existing network options, and add the new one
  187. container.args = [a for a in container.args
  188. if not a.startswith('--network=')]
  189. container.args.extend(['--network=%s' % args.network])
  190. def modify_port_add(parser=None, args=None, container=None):
  191. """Allows a additional ports to be exposed."""
  192. if parser:
  193. parser.add_argument('--port', '-p', action='append',
  194. help='Additional port to expose')
  195. elif args.port:
  196. container.args.extend(['-p=%s' % port for port in args.port])
  197. def modify_tag(parser=None, args=None, container=None):
  198. """Allows the tag (version) to be updated."""
  199. if parser:
  200. parser.add_argument('--tag',
  201. help='Image tag (version) to use')
  202. elif args.tag:
  203. # Get rid of any existing digest or tag
  204. image = container.image.replace('@', ':')
  205. image = image.split(':')[0]
  206. container.image = '%s:%s' % (image, args.tag)
  207. def functions():
  208. """Lists all functions defined in this module.
  209. Returns:
  210. list of (str,function): List of (name, function) pairs for each
  211. function defined in this module.
  212. """
  213. return [m for m
  214. in inspect.getmembers(sys.modules[__name__])
  215. if inspect.isfunction(m[1])]
  216. def handlers():
  217. """Lists all handlers defined in this module.
  218. Returns:
  219. list of function: All handlers (handle_* funcs) defined in this module.
  220. """
  221. return [func for (name, func) in functions() if name.startswith('handle_')]
  222. def modifiers():
  223. """Lists all modifiers defined in this module.
  224. Returns:
  225. list of function: All modifiers (modify_* funcs) in this module.
  226. """
  227. return [func for (name, func) in functions() if name.startswith('modify_')]
  228. def main(argv, out):
  229. """Script entry point."""
  230. parser = argparse.ArgumentParser(description='Reruns docker containers ' \
  231. 'with different parameters.')
  232. parser.add_argument('container', type=str, help='The container to rerun')
  233. parser.add_argument('-d', '--dry-run', action='store_true',
  234. help='Don\'t actually re-run the container, just ' \
  235. 'print what would happen.')
  236. parser.add_argument('--pull', action='store_true',
  237. help='Docker pull the image before re-running the ' \
  238. 'container')
  239. mods = modifiers()
  240. for mod in mods:
  241. mod(parser=parser)
  242. args = parser.parse_args(argv)
  243. container_info = docker_inspect(args.container, 'container')
  244. image_info = docker_inspect(container_info['Config']['Image'], 'image')
  245. container = Container(container_info, image_info)
  246. for handler in handlers():
  247. handler(container)
  248. for mod in mods:
  249. mod(args=args, container=container)
  250. commands = [
  251. ['docker', 'stop', args.container],
  252. ['docker', 'rm', args.container],
  253. container.command_line(),
  254. ]
  255. if args.pull:
  256. commands = [['docker', 'pull', container.image]] + commands
  257. if args.dry_run:
  258. print('Performing dry run for container %s. The following would be ' \
  259. 'executed:' % args.container, file=out)
  260. for command in commands:
  261. print(' '.join(command), file=out)
  262. else:
  263. print('Re-running container %s...' % args.container, file=out)
  264. for command in commands:
  265. subprocess.check_call(command)
  266. def entrypoint():
  267. """Entrypoint for script use."""
  268. main(sys.argv[1:], sys.stdout)
  269. if __name__ == "__main__":
  270. entrypoint()