diff --git a/Dockerfile b/Dockerfile index c6327b6..26deb42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,54 @@ FROM tiangolo/uwsgi-nginx-flask:flask -COPY * /app +RUN apt-get update && apt-get upgrade --fix-missing -y +RUN apt-get update && apt-get install -y curl git bzr mercurial build-essential +RUN apt-get install -y zip ruby-full haskell-platform shellcheck ssh +RUN apt-get install -y python-pip python-dev +RUN apt-get install -y nodejs build-essential golang -RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - -RUN apt-get update && apt-get install -y ruby-full haskell-platform shellcheck nodejs build-essential nodejs-legacy +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION node +# Install nvm with node and npm +RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash \ + && . $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default \ + && npm install -g jsonlint jscs eslint jshint + +ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH + +RUN echo '. "$NVM_DIR/nvm.sh"' >> /etc/profile +RUN echo NODE_VERSION=$NODE_VERSION >> /etc/environment +RUN echo NVM_DIR=$NVM_DIR >> /etc/environment +RUN echo NODE_PATH=$NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules >> /etc/environment +RUN echo PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH >> /etc/environment + +RUN pip install -U pip +RUN curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash +RUN export PATH="/root/.pyenv/bin:$PATH" +ENV PATH /root/.pyenv/bin:$PATH +RUN eval "$(pyenv init -)" +RUN eval "$(pyenv virtualenv-init -)" +RUN pyenv update +RUN pyenv install 2.7.13 +RUN pyenv install 3.6.0 +RUN pyenv global 2.7.13 3.6.0 +COPY ./app /app +COPY requirements.txt /app RUN pip install -r /app/requirements.txt + +# Install rvm (https://github.com/vallard/docker/blob/master/rails/Dockerfile) +RUN apt-get install -y curl patch gawk g++ gcc make libc6-dev patch libreadline6-dev zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 autoconf libgdbm-dev libncurses5-dev automake libtool bison pkg-config libffi-dev +RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 +RUN curl -L https://get.rvm.io | bash -s stable +RUN bash -l -c "rvm requirements" +RUN bash -l -c "rvm install 2.0" +RUN bash -l -c "gem install bundler --no-ri --no-rdoc" + +RUN mkdir -p /root/.ssh +RUN touch /root/.ssh/known_hosts +RUN chmod 0700 /root/.ssh +RUN chmod 0600 /root/.ssh/known_hosts diff --git a/Procfile b/Procfile index 629b83a..0aeea5f 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: python main.py +web: python app/main.py diff --git a/main.py b/app/main.py similarity index 60% rename from main.py rename to app/main.py index 12bfd33..217e5f8 100644 --- a/main.py +++ b/app/main.py @@ -4,7 +4,10 @@ from __future__ import print_function from __future__ import unicode_literals +import hashlib import os +import random +import re import shutil import subprocess import tempfile @@ -12,6 +15,7 @@ import time import traceback from flask import Flask, request, redirect +import github3 app = Flask(__name__) @@ -20,7 +24,37 @@ SAFE_ENV['TOKEN'] = '' DOTFILES = 'dotfiles' STOP_FILE_NAME = '.inlineplzstop' +SSH_FILE_NAME = 'inline_plz_rsa' +SSH_FILE_PATH = os.path.join(os.path.expanduser('~'), '.ssh', SSH_FILE_NAME) REVIEWS_IN_PROGRESS = dict() +SSH_LOCK = threading.Lock() + + +def ssh_keygen(): + time.sleep(random.randint(1, 10)) + while not os.path.exists(SSH_FILE_PATH): + try: + subprocess.check_call(['ssh-keygen', '-t', 'rsa', '-b', '2048', '-f', SSH_FILE_PATH, '-q', '-N', '']) + ssh_output = subprocess.check_output('ssh-agent -s', shell=True, stderr=subprocess.STDOUT) + # http://code.activestate.com/recipes/533143-set-environment-variables-for-using-ssh-in-python-/ + for sh_line in ssh_output.splitlines(): + matches=re.search("(\S+)\=(\S+)\;", sh_line) + if matches: + os.environ[matches.group(1)]=matches.group(2) + SAFE_ENV[matches.group(1)]=matches.group(2) + subprocess.check_call('ssh-add {}'.format(SSH_FILE_PATH), shell=True) + except Exception: + traceback.print_exc() + time.sleep(random.randint(1, 10)) + + +TRUSTED = os.environ.get('TRUSTED', '').lower().strip() in ['true', 'yes', '1'] +if TRUSTED: + ssh_keygen() + +SSH_KEY_HASH = hashlib.md5() +SSH_KEY_HASH.update(open(SSH_FILE_PATH).read()) +SSH_KEY_HASH = SSH_KEY_HASH.hexdigest()[-6:] @app.errorhandler(Exception) @@ -63,6 +97,44 @@ def clone_dotfiles(url, org, tempdir, token): return clone(clone_url, dotfile_path, token) +def ssh_setup(url, token): + with SSH_LOCK: + try: + with open(os.path.join(os.path.expanduser('~'), '.ssh', 'config'), 'ar+') as sshconfig: + contents = sshconfig.read() + if not 'HostName {}'.format(url) in contents: + sshconfig.write('\nHost {0}\n\tHostName {0}\n\tIdentityFile {1}'.format(url, SSH_FILE_PATH)) + except Exception: + traceback.print_exc() + if not url or url in ['http://github.com', 'https://github.com']: + github = github3.GitHub(token=token) + else: + github = github3.GitHubEnterprise(url, token=token) + + key_found = False + for key in github.iter_keys(): + if SSH_FILE_NAME in key.title and not SSH_KEY_HASH in key.title: + github.delete_key(key.id) + elif key.title == '{}_{}'.format(SSH_FILE_NAME, SSH_KEY_HASH): + key_found = True + if not key_found: + github.create_key('{}_{}'.format(SSH_FILE_NAME, SSH_KEY_HASH), open(SSH_FILE_PATH + '.pub').read()) + + keygen_url = url.split('//')[-1] + try: + output = subprocess.check_output(['ssh-keygen', '-F', keygen_url], stderr=subprocess.STDOUT) + if output.strip(): + return + except subprocess.CalledProcessError: + traceback.print_exc() + try: + output = subprocess.check_output(['ssh-keyscan', '-t', 'rsa', keygen_url], stderr=subprocess.STDOUT) + with open(os.path.join(os.path.expanduser('~'), '.ssh', 'known_hosts'), 'a') as known_hosts: + known_hosts.write(output) + except Exception: + traceback.print_exc() + + def lint(data): try: pull_request = data['pull_request']['number'] @@ -70,7 +142,7 @@ def lint(data): name = data['repository']['name'] token = os.environ.get('TOKEN') interface = 'github' - url = os.environ.get('URL', 'https://github.com') + url = 'https://' + data['repository']['ssh_url'].split('@')[1].split(':')[0] event_type = data['action'] sha = data['pull_request']['head']['sha'] ref = data['pull_request']['head']['ref'] @@ -79,7 +151,7 @@ def lint(data): except KeyError: traceback.print_exc() return 'Invalid pull request data.' - trusted = os.environ.get('TRUSTED', '').lower().strip() in ['true', 'yes', '1'] + print('Starting inline-plz:') print('Event: {}'.format(event_type)) @@ -92,6 +164,9 @@ def lint(data): if event_type not in ['opened', 'synchronize']: return + if TRUSTED: + ssh_setup(url, token) + # make temp dirs tempdir = tempfile.mkdtemp() dotfile_dir = tempfile.mkdtemp() @@ -130,7 +205,7 @@ def lint(data): '--interface={}'.format(interface), '--zero-exit' ] - if trusted: + if TRUSTED: args.append('--trusted') if clone_dotfiles(url, org, dotfile_dir, token): args.append('--config-dir={}'.format( diff --git a/requirements.txt b/requirements.txt index 8f2968d..a1c32eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ configparser==3.5.0 docutils==0.12 dodgy==0.1.9 Flask==0.11.1 -github3.py==0.9.5 +github3.py==0.9.6 inlineplz==0.23.1 isort==4.2.5 itsdangerous==0.24