diff --git a/repo2docker/app.py b/repo2docker/app.py index 709409b3..7c75b48f 100644 --- a/repo2docker/app.py +++ b/repo2docker/app.py @@ -13,6 +13,7 @@ import time import logging import uuid import shutil +import argparse from pythonjsonlogger import jsonlogger import escapism @@ -47,51 +48,10 @@ class Repo2Docker(Application): version = __version__ description = __doc__ - config_file = Unicode( - 'repo2docker_config.py', - config=True, - help=""" - Path to read traitlets configuration file from. - """ - ) - @default('log_level') def _default_log_level(self): return logging.INFO - repo = Unicode( - os.getcwd(), - allow_none=True, - config=True, - help=""" - The git repository to clone. - - Could be a git URL or a file path. - """ - ) - - ref = Unicode( - 'master', - allow_none=True, - config=True, - help=""" - The git ref in the git repository to build. - - Can be a tag, ref or branch. - """ - ) - - output_image_spec = Unicode( - None, - allow_none=True, - config=True, - help=""" - The spec of the image to build. - - Should be the same as the value passed to `-t` param of docker build. - """ - ) - git_workdir = Unicode( "/tmp", config=True, @@ -149,27 +109,6 @@ class Repo2Docker(Application): DANGEROUS WHEN DONE IN A CLOUD ENVIRONMENT! ONLY USE LOCALLY! """ ) - json_logs = Bool( - False, - config=True, - help=""" - Enable JSON logging for easier consumption by external services. - """ - ) - - aliases = Dict({ - 'repo': 'Repo2Docker.repo', - 'ref': 'Repo2Docker.ref', - 'image': 'Repo2Docker.output_image_spec', - 'f': 'Repo2Docker.config_file', - }) - - flags = Dict({ - 'no-clean': ({'Repo2Docker': {'cleanup_checkout': False}}, 'Do not clean up git checkout'), - 'no-run': ({'Repo2Docker': {'run': False}}, 'Do not run built container image'), - 'push': ({'Repo2Docker': {'push': True}}, 'Push built image to a docker registry'), - 'json-logs': ({'Repo2Docker': {'json_logs': True}}, 'Enable JSON logging'), - }) def fetch(self, url, ref, checkout_path): try: @@ -180,19 +119,74 @@ class Repo2Docker(Application): self.log.error('Failed to clone repository!', extra=dict(phase='failed')) sys.exit(1) - try: - for line in execute_cmd(['git', 'reset', '--hard', ref], cwd=checkout_path, - capture=self.json_logs): - self.log.info(line, extra=dict(phase='fetching')) - except subprocess.CalledProcessError: - self.log.error('Failed to check out ref %s', ref, extra=dict(phase='failed')) - sys.exit(1) + if ref: + try: + for line in execute_cmd(['git', 'reset', '--hard', ref], cwd=checkout_path, + capture=self.json_logs): + self.log.info(line, extra=dict(phase='fetching')) + except subprocess.CalledProcessError: + self.log.error('Failed to check out ref %s', ref, extra=dict(phase='failed')) + sys.exit(1) - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - self.load_config_file(self.config_file) + def get_argparser(self): + argparser = argparse.ArgumentParser() + argparser.add_argument( + '--config', + default='repo2docker_config.py', + help="Path to config file for repo2docker" + ) - if self.json_logs: + argparser.add_argument( + '--json-logs', + default=False, + action='store_true', + help='Emit JSON logs instead of human readable logs' + ) + + argparser.add_argument( + 'repo', + help='Path to repository that should be built' + ) + + argparser.add_argument( + '--image-name', + help='Name of image to be built. If unspecified will be autogenerated' + ) + + argparser.add_argument( + '--push', + dest='push', + action='store_true', + help='Push docker image to repository' + ) + + argparser.add_argument( + '--no-run', + dest='run', + action='store_false', + help='Do not run container after it has been built' + ) + + argparser.add_argument( + 'cmd', + nargs='*', + help='Custom command to run after building container' + ) + + return argparser + + def initialize(self): + args = self.get_argparser().parse_args() + + self.load_config_file(args.config) + + if '@' in args.repo: + self.repo, self.ref = args.repo.split('@', 1) + else: + self.repo = args.repo + self.ref = None + + if args.json_logs: # Need to reset existing handlers, or we repeat messages logHandler = logging.StreamHandler() formatter = jsonlogger.JsonFormatter() @@ -208,18 +202,18 @@ class Repo2Docker(Application): # We don't want a [Repo2Docker] on all messages self.log.handlers[0].formatter = logging.Formatter(fmt='%(message)s') - if len(self.extra_args) == 1: - # accept repo as a positional arg - self.repo = self.extra_args[0] - elif len(self.extra_args) > 1: - print("%s accepts at most one positional argument." % self.name, file=sys.stderr) - print("See python -m repo2docker --help for usage", file=sys.stderr) - self.exit(1) - - if self.output_image_spec is None: + if args.image_name: + self.output_image_spec = args.image_name + else: # Attempt to set a sane default! # HACK: Provide something more descriptive? - self.output_image_spec = escapism.escape(self.repo).lower() + ':' + self.ref.lower() + self.output_image_spec = 'image-' + escapism.escape(self.repo).lower() + + self.push = args.push + self.run = args.run + self.json_logs = args.json_logs + + self.run_cmd = args.cmd def push_image(self): @@ -245,11 +239,18 @@ class Repo2Docker(Application): def run_image(self): client = docker.from_env(version='auto') port = self._get_free_port() + if not self.run_cmd: + port = str(self._get_free_port()) + run_cmd = ['jupyter', 'notebook', '--ip', '0.0.0.0', '--port', port] + ports={'%s/tcp' % port: port} + else: + run_cmd = self.run_cmd + ports = {} container = client.containers.run( self.output_image_spec, - ports={'%s/tcp' % port: port}, + ports={}, detach=True, - command=['jupyter', 'notebook', '--ip', '0.0.0.0', '--port', str(port)], + command=run_cmd ) while container.status == 'created': time.sleep(0.5) @@ -259,8 +260,10 @@ class Repo2Docker(Application): for line in container.logs(stream=True): self.log.info(line.decode('utf-8'), extra=dict(phase='running')) finally: + # FIXME: We should pass the container's exit code back out! self.log.info('Stopping container...\n', extra=dict(phase='running')) - container.kill() + if container.status == 'running': + container.kill() container.remove() def _get_free_port(self):