diff --git a/README.md b/README.md index 7326b4f..c9fe711 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ information. pip install nbgitpuller ``` +### Configuration + +Copy `jupyter_git_pull_config.py` to one of your Jupyter configuration paths (as determined from `jupyter --paths`) and edit it to meet your needs. + ## Example This example shows how to use the [nbgitpuller link generator] diff --git a/jupyter_git_pull_config.py b/jupyter_git_pull_config.py new file mode 100644 index 0000000..3719c4d --- /dev/null +++ b/jupyter_git_pull_config.py @@ -0,0 +1,34 @@ +# May be set to a list of URLs described as Python regular expressions (using re.fullmatch()) +# where it is permitted to autorun scripts from the pulled project as a pre-initialisation +# step. +# +# WARNING: Enable this only if you understand and accept the risks of AUTORUN.INF. +# ---- +# c.NbGitPuller.autorun_allow = [ +# r'https://github\.com/org/name\.git', +# r'https://github\.com/org-two/name-two\.git' +# ] +# ---- +# +# To allow all sources (*not* recommended) use: +# ---- +# c.NbGitPuller.autorun_allow = True +# ---- +# +# The default is 'False' which means the autorun functionality is completely disabled +#c.NbGitPuller.autorun_allow = False + +# List of scripts to search for when attempting to autorun. The first match will +# be run with a single argument of 'init' or 'update' depending on what nbgitpuller +# is doing. +# ---- +# c.NbGitPuller.autorun_script = [ +# '.nbgitpuller.script', +# '.different.script' +# ] +# ---- +# +# The 'script' must be executable and when checked out on a 'exec' (ie. not a 'noexec') mountpoint +# +# The default is the empty list. +#c.NbGitPuller.autorun_script = [] diff --git a/nbgitpuller/application.py b/nbgitpuller/application.py index 409cec3..9b878b3 100644 --- a/nbgitpuller/application.py +++ b/nbgitpuller/application.py @@ -1,6 +1,8 @@ from .version import __version__ # noqa from .pull import GitPuller # noqa from jupyter_server.extension.application import ExtensionApp +from traitlets import Bool, CRegExp, List, Unicode, Union +from traitlets.config import Configurable import os @@ -12,6 +14,33 @@ class NbGitPuller(ExtensionApp): os.path.join(os.path.dirname(__file__), 'static') ] + autorun_allow = Union( + [Bool(), List(CRegExp())], + default_value=False, + config=True, + help=""" + List of URLs described as Python regular expressions (using re.fullmatch()) where + it is permitted to autorun scripts from the pulled project as a pre-initialisation + step. Enable this only if you understand and accept the risks of AUTORUN.INF. + + When set to boolean True, all URLs are allowed, whilst False (default) autorun + is disabled completely. + """ + ) + + autorun_script = List( + Unicode(), + default_value=[], + config=True, + help=""" + List of scripts to search for when attempting to autorun. The first match will + be run with a single argument of 'init' or 'update' depending on what nbgitpuller + is doing. + + Enable this only if you understand and accept the risks of AUTORUN.INF. + """ + ) + def initialize_handlers(self): from .handlers import ( SyncHandler, diff --git a/nbgitpuller/pull.py b/nbgitpuller/pull.py index dd8aae1..3d87ca3 100644 --- a/nbgitpuller/pull.py +++ b/nbgitpuller/pull.py @@ -1,4 +1,5 @@ import os +import re import subprocess import logging import time @@ -81,6 +82,9 @@ def __init__(self, git_url, repo_dir, branch, **kwargs): elif not self.branch_exists(self.branch_name): raise ValueError(f"Branch: {self.branch_name} -- not found in repo: {self.git_url}") + self.autorun_allow = kwargs.pop('autorun_allow', False) + self.autorun_script = kwargs.pop('autorun_script', []) + newargs = {k: v for k, v in kwargs.items() if v is not None} super(GitPuller, self).__init__(**newargs) @@ -143,6 +147,30 @@ def pull(self): else: yield from self.update() + def autorun(self, operation="method"): + """ + Search for and execute the autorun script. + """ + + if not self.autorun_allow: + return + if not any(( re.fullmatch(pattern, self.git_url) for pattern in self.autorun_allow )): + logging.info('autorun skipped, URL does not match any rules') + return + + script = next(( s for s in self.autorun_script if os.access(os.path.join(self.repo_dir, s), os.X_OK)), None) + if not script: + logging.info('autorun skipped, no matching (executable) script') + return + + try: + for line in execute_cmd([ os.path.join(self.repo_dir, script), operation ], cwd=self.repo_dir, close_fds=True): + yield line + except subprocess.CalledProcessError: + m = f"Problem autorunning {script}" + logging.exception(m) + raise ValueError(m) + def initialize_repo(self): """ Clones repository @@ -154,6 +182,7 @@ def initialize_repo(self): clone_args.extend(['--branch', self.branch_name]) clone_args.extend(["--", self.git_url, self.repo_dir]) yield from execute_cmd(clone_args) + yield from self.autorun('init') logging.info('Repo {} initialized'.format(self.repo_dir)) def reset_deleted_files(self): @@ -343,6 +372,8 @@ def update(self): yield from self.ensure_lock() yield from self.merge() + yield from self.autorun('update') + def main(): """