Switch to using argparse for commandline parsing

Is cleaner and less hacky than using Traitlets. We still
use traitlets for configuration, and accept a config file
for configuring detectors and what not.

Also implements allowing arbitrary commands from the commandline,
so we can do things like build and test a container!
pull/42/head
yuvipanda 2017-07-28 23:46:04 -07:00
rodzic c1816a82d4
commit 64bf17513f
1 zmienionych plików z 89 dodań i 86 usunięć

Wyświetl plik

@ -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):