diff --git a/Makefile b/Makefile index 366a4bba2..c2dda8be0 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,6 @@ check: .PHONY: build # Build all docker images build: docker-compose build --parallel $(BUILD_ALLWAYS) - ./pipeline/build.sh .PHONY: push # Push images to registry.osiris.services (requires vpn) push: build diff --git a/api/Dockerfile b/api/Dockerfile index 8096043b4..8f9dcb429 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /opt/app COPY requirements.txt requirements.txt -RUN apk add --update --no-cache mariadb-client git curl tzdata gcc py3-gevent \ +RUN apk add --update --no-cache mariadb-client git curl tzdata gcc musl-dev \ && pip3 install -r ./requirements.txt \ && adduser -D anubis \ && chown anubis:anubis -R /opt/app \ diff --git a/api/Makefile b/api/Makefile index eeef162e8..86db2caed 100644 --- a/api/Makefile +++ b/api/Makefile @@ -2,7 +2,7 @@ all: venv venv: - virtualenv -p `which python3` venv + virtualenv -p `which python3.8` venv ./venv/bin/pip install -r ./requirements.txt diff --git a/api/anubis/app.py b/api/anubis/app.py index 1d04711d3..06bdc621d 100644 --- a/api/anubis/app.py +++ b/api/anubis/app.py @@ -4,10 +4,6 @@ from flask import Flask from anubis.utils.logger import logger -logger.setLevel(logging.DEBUG) -logger.addHandler(logging.StreamHandler()) - - def init_services(app): """ Initialize app with redis cache, mariadb database, and ELK services @@ -37,12 +33,11 @@ def init_services(app): def index(): return 'Hello there...!' + # Make app logger anubis logger + app.logger = logger + # Add ELK stuff if not config.DISABLE_ELK: - # Add logstash handler - logger.addHandler(logstash.LogstashHandler('logstash', 5000)) - app.logger = logger - # Add elastic global error handler add_global_error_handler(app) diff --git a/api/anubis/config.py b/api/anubis/config.py index a9baaac70..51eb163cd 100644 --- a/api/anubis/config.py +++ b/api/anubis/config.py @@ -17,10 +17,15 @@ class Config: OAUTH_CONSUMER_KEY = '' OAUTH_CONSUMER_SECRET = '' - ADMINS = os.environ.get('ADMINS', 'jmc1283@nyu.edu') - + # Cache config CACHE_REDIS_HOST = 'redis' + # Logger config + LOGGER_NAME = os.environ.get('LOGGER_NAME', default='anubis-api') + + # Theia config + THEIA_DOMAIN = os.environ.get('THEIA_DOMAIN', default='ide.anubis.osiris.services') + def __init__(self): self.DEBUG = os.environ.get('DEBUG', default='0') == '1' self.SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI', @@ -35,6 +40,12 @@ def __init__(self): # Redis self.CACHE_REDIS_HOST = os.environ.get('CACHE_REDIS_HOST', default='redis') + # Logger + self.LOGGER_NAME = os.environ.get('LOGGER_NAME', default='anubis-api') + + # Theia + self.THEIA_DOMAIN = os.environ.get('THEIA_DOMAIN', default='ide.anubis.osiris.services') + logging.info('Starting with DATABASE_URI: {}'.format( self.SQLALCHEMY_DATABASE_URI)) logging.info('Starting with SECRET_KEY: {}'.format(self.SECRET_KEY)) diff --git a/api/anubis/models/__init__.py b/api/anubis/models/__init__.py index 6cd5e4bb4..e4ff6d09f 100644 --- a/api/anubis/models/__init__.py +++ b/api/anubis/models/__init__.py @@ -62,15 +62,14 @@ class Class_(db.Model): @property def total_assignments(self): - return len(list(self.assignments)) + return self.open_assignments @property def open_assignments(self): now = datetime.now() return Assignment.query.filter( Assignment.class_id == self.id, - Assignment.release_date >= now, - Assignment.due_date <= now + Assignment.release_date <= now, ).count() @property @@ -113,6 +112,7 @@ class Assignment(db.Model): github_classroom_url = db.Column(db.String(256), nullable=True) pipeline_image = db.Column(db.String(256), unique=True, nullable=True) unique_code = db.Column(db.String(8), unique=True, default=lambda: base64.b16encode(os.urandom(4)).decode()) + ide_enabled = db.Column(db.Boolean, default=True) # Dates release_date = db.Column(db.DateTime, nullable=False) @@ -132,7 +132,6 @@ def data(self): 'course': self.class_.data, 'description': self.description, 'github_classroom_link': self.github_classroom_url, - 'tests': [t.data for t in self.tests] } @@ -172,6 +171,15 @@ class AssignmentRepo(db.Model): assignment = db.relationship(Assignment, cascade='all,delete') submissions = db.relationship('Submission', cascade='all,delete') + @property + def data(self): + return { + 'github_username': self.github_username, + 'assignment_name': self.assignment.name, + 'class_name': self.assignment.class_.class_code, + 'repo_url': self.repo_url, + } + class AssignmentTest(db.Model): __tablename__ = 'assignment_test' @@ -313,6 +321,8 @@ def init_submission_models(self): :return: """ + logging.info('initializing submission {}'.format(self.id)) + # If the models already exist, yeet if len(self.test_results) != 0: SubmissionTestResult.query.filter_by(submission_id=self.id).delete() @@ -325,7 +335,7 @@ def init_submission_models(self): # Find tests for the current assignment tests = AssignmentTest.query.filter_by(assignment_id=self.assignment_id).all() - logging.error('found tests: {}'.format(list(map(lambda x: x.data, tests)))) + logging.debug('found tests: {}'.format(list(map(lambda x: x.data, tests)))) for test in tests: tr = SubmissionTestResult(submission=self, assignment_test=test) @@ -334,7 +344,7 @@ def init_submission_models(self): db.session.add(sb) self.processed = False - self.state = 'Reset' + self.state = 'Waiting for resources...' db.session.add(self) # Commit new models @@ -379,7 +389,6 @@ def data(self): 'assignment_name': self.assignment.name, 'assignment_due': str(self.assignment.due_date), 'class_code': self.assignment.class_.class_code, - 'url': self.url, 'commit': self.commit, 'processed': self.processed, 'state': self.state, @@ -480,3 +489,50 @@ def stat_data(self): data = self.data del data['stdout'] return data + + +class TheiaSession(db.Model): + __tablename__ = 'theia_session' + + # id + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + + owner_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) + assignment_id = db.Column(db.Integer, db.ForeignKey(Assignment.id), nullable=False) + repo_id = db.Column(db.Integer, db.ForeignKey(AssignmentRepo.id), nullable=False) + + active = db.Column(db.Boolean, default=True) + state = db.Column(db.String(128)) + cluster_address = db.Column(db.String(256), nullable=True, default=None) + + # Timestamps + created = db.Column(db.DateTime, default=datetime.now) + ended = db.Column(db.DateTime, nullable=True, default=None) + last_heartbeat = db.Column(db.DateTime, default=datetime.now) + last_proxy = db.Column(db.DateTime, default=datetime.now) + last_updated = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) + + repo = db.relationship(AssignmentRepo) + owner = db.relationship(User) + assignment = db.relationship(Assignment) + + @property + def data(self): + from anubis.utils.data import theia_redirect_url + + return { + 'id': self.id, + 'assignment_id': self.assignment_id, + 'assignment_name': self.assignment.name, + 'class_name': self.assignment.class_.class_code, + 'repo_id': self.repo_id, + 'repo_url': self.repo.repo_url, + 'redirect_url': theia_redirect_url(self.id, self.owner.netid), + 'active': self.active, + 'state': self.state, + 'created': str(self.created), + 'ended': str(self.ended), + 'last_heartbeat': str(self.last_heartbeat), + 'last_proxy': str(self.last_proxy), + 'last_updated': str(self.last_updated), + } diff --git a/api/anubis/routes/pipeline.py b/api/anubis/routes/pipeline.py index e2a575793..3880e466f 100644 --- a/api/anubis/routes/pipeline.py +++ b/api/anubis/routes/pipeline.py @@ -34,7 +34,7 @@ def pipeline_report_panic(submission: Submission): 'owner_id': submission.owner_id, 'data': json.dumps(request.json)}) submission.processed = True - submission.state = 'Whoops! There was an error on our end. The Anubis admins have been notified.' + submission.state = 'Whoops! There was an error on our end. The error has been logged.' submission.errors = {'panic': request.json} db.session.add(submission) diff --git a/api/anubis/routes/private.py b/api/anubis/routes/private.py index f2fcf2c47..0f9a23747 100644 --- a/api/anubis/routes/private.py +++ b/api/anubis/routes/private.py @@ -7,16 +7,21 @@ from flask import request, Blueprint, Response from sqlalchemy import or_, and_ -from anubis.models import db, User, Submission, Assignment, AssignmentQuestion, AssignedStudentQuestion +from anubis.models import db, User, Submission +from anubis.models import Assignment, AssignmentQuestion, AssignedStudentQuestion +from anubis.models import TheiaSession from anubis.utils.auth import get_token from anubis.utils.cache import cache -from anubis.utils.data import regrade_submission, is_debug +from anubis.utils.data import regrade_submission, bulk_regrade_submission, is_debug from anubis.utils.data import success_response, error_response +from anubis.utils.data import bulk_stats, get_students, get_assigned_questions +from anubis.utils.data import fix_dangling, _verify_data_shape, split_chunks from anubis.utils.decorators import json_response, json_endpoint, load_from_id from anubis.utils.elastic import log_endpoint -from anubis.utils.redis_queue import enqueue_webhook_rpc +from anubis.utils.redis_queue import enqueue_webhook, rpc_enqueue from anubis.utils.logger import logger -from anubis.utils.data import fix_dangling, bulk_stats, get_students, _verify_data_shape +from anubis.rpc.batch import rpc_bulk_regrade +from anubis.rpc.theia import reap_all_theia_sessions private = Blueprint('private', __name__, url_prefix='/private') @@ -37,6 +42,28 @@ def private_token_netid(netid): return res +@private.route('/assignment//questions/get/') +@log_endpoint('cli', lambda: 'question get') +@load_from_id(Assignment, verify_owner=False) +@json_response +def private_assignment_id_questions_get_netid(assignment: Assignment, netid: str): + """ + Get questions assigned to a given student + + :param assignment: + :param netid: + :return: + """ + user = User.query.filter_by(netid=netid).first() + if user is None: + return error_response('user not found') + + return success_response({ + 'netid': user.netid, + 'questions': get_assigned_questions(assignment.id, user.id) + }) + + @private.route('/assignment//questions/assign') @log_endpoint('cli', lambda: 'question assign') @load_from_id(Assignment, verify_owner=False) @@ -267,7 +294,7 @@ def private_regrade_submission(commit): return error_response('not found') s.init_submission_models() - enqueue_webhook_rpc(s.id) + enqueue_webhook(s.id) return success_response({ 'submission': s.data, @@ -300,23 +327,18 @@ def private_regrade_assignment(assignment_name): if assignment is None: return error_response('cant find assignment') - submission = Submission.query.filter( + submissions = Submission.query.filter( Submission.assignment_id == assignment.id, Submission.owner_id != None ).all() - response = [] + submission_ids = [s.id for s in submissions] + submission_chunks = split_chunks(submission_ids, 100) - for s in submission: - res = regrade_submission(s) - response.append({ - 'submission': s.id, - 'commit': s.commit, - 'netid': s.netid, - 'success': res['success'], - }) + for chunk in submission_chunks: + rpc_enqueue(rpc_bulk_regrade, chunk) - return success_response({'submissions': response}) + return success_response({'status': 'chunks enqueued'}) @private.route('/fix-dangling') @@ -332,6 +354,10 @@ def private_fix_dangling(): @json_response def private_stats_assignment(assignment_id, netid=None): netids = request.args.get('netids', None) + force = request.args.get('force', False) + + if force is not False: + cache.clear() if netids is not None: netids = json.loads(netids) @@ -344,6 +370,35 @@ def private_stats_assignment(assignment_id, netid=None): return success_response({'stats': bests}) +@private.route('/submission/') +@log_endpoint('cli', lambda: 'submission-stats') +@load_from_id(Submission, verify_owner=False) +@json_response +def private_submission_stats_id(submission: Submission): + """ + Get full stats for a specific submission. + + :param submission: + :return: + """ + + return success_response({ + 'student': submission.owner.data, + 'submission': submission.full_data, + 'assignment': submission.assignment.data, + 'class': submission.assignment.class_.data, + }) + + +@private.route('/ide/clear') +@log_endpoint('cli', lambda: 'clear-ide') +@json_response +def private_ide_clear(): + rpc_enqueue(reap_all_theia_sessions, tuple()) + + return success_response({'state': 'enqueued'}) + + from anubis.models import SubmissionTestResult, SubmissionBuild from anubis.models import AssignmentTest, AssignmentRepo, InClass, Class_ @@ -407,6 +462,6 @@ def private_seed(): a1s2.init_submission_models() a2s1.init_submission_models() - enqueue_webhook_rpc(a2s1.id) + enqueue_webhook(a2s1.id) return success_response('seeded') diff --git a/api/anubis/routes/public.py b/api/anubis/routes/public.py index 5c04e4389..6dbab7ac9 100644 --- a/api/anubis/routes/public.py +++ b/api/anubis/routes/public.py @@ -1,9 +1,12 @@ import string from datetime import datetime, timedelta +from typing import List, Dict from flask import request, redirect, Blueprint, make_response -from anubis.models import Assignment, AssignmentRepo, AssignmentQuestion, AssignedStudentQuestion +from sqlalchemy import distinct + +from anubis.models import Assignment, AssignmentRepo, AssignedStudentQuestion, TheiaSession from anubis.models import Submission from anubis.models import db, User, Class_, InClass from anubis.utils.auth import current_user @@ -11,17 +14,33 @@ from anubis.utils.cache import cache from anubis.utils.data import error_response, success_response, is_debug from anubis.utils.data import fix_dangling -from anubis.utils.data import get_classes, get_assignments, get_submissions -from anubis.utils.data import regrade_submission, enqueue_webhook_rpc +from anubis.utils.data import get_classes, get_assignments, get_submissions, get_assigned_questions +from anubis.utils.data import regrade_submission +from anubis.utils.data import theia_redirect, theia_list_all, theia_poll_ide, theia_redirect_url from anubis.utils.decorators import json_endpoint, json_response, require_user, load_from_id from anubis.utils.elastic import log_endpoint, esindex from anubis.utils.http import get_request_ip from anubis.utils.logger import logger from anubis.utils.oauth import OAUTH_REMOTE_APP as provider +from anubis.utils.redis_queue import enqueue_ide_initialize, enqueue_webhook, enqueue_ide_stop public = Blueprint('public', __name__, url_prefix='/public') +def webhook_log_msg(): + if request.headers.get('Content-Type', None) == 'application/json' and \ + request.headers.get('X-GitHub-Event', None) == 'push': + return request.json['pusher']['name'] + return None + + +@public.route('/memes') +@log_endpoint('rick-roll', lambda: 'rick-roll') +def public_memes(): + logger.info('rick-roll') + return redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1') + + @public.route('/login') @log_endpoint('public-login', lambda: 'login') def public_login(): @@ -163,17 +182,8 @@ def public_assignment_questions_id(assignment: Assignment): # Load current user user: User = current_user() - # Get assigned questions - assigned_questions = AssignedStudentQuestion.query.filter( - AssignedStudentQuestion.assignment_id == assignment.id, - AssignedStudentQuestion.owner_id == user.id, - ).all() - return success_response({ - 'questions': [ - assigned_question.data - for assigned_question in assigned_questions - ] + 'questions': get_assigned_questions(assignment.id, user.id) }) @@ -264,20 +274,6 @@ def public_submission(commit: str): return success_response({'submission': s.full_data}) -def webhook_log_msg(): - if request.headers.get('Content-Type', None) == 'application/json' and \ - request.headers.get('X-GitHub-Event', None) == 'push': - return request.json['pusher']['name'] - return None - - -@public.route('/memes') -@log_endpoint('rick-roll', lambda: 'rick-roll') -def public_memes(): - logger.info('rick-roll') - return redirect('https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1') - - @public.route('/regrade/') @require_user @log_endpoint('regrade-request', lambda: 'submission regrade request ' + request.path) @@ -348,6 +344,22 @@ def public_webhook(): 'repo_url': repo_url, 'github_username': github_username, 'assignment_name': assignment_name, 'commit': commit, }) + + repo_name_split = webhook['repository']['name'].split('-') + unique_code_index = repo_name_split.index(assignment.unique_code) + repo_name_split = repo_name_split[unique_code_index+1:] + github_username1 = '-'.join(repo_name_split) + github_username2 = '-'.join(repo_name_split[:-1]) + user = User.query.filter( + User.github_username.in_([github_username1, github_username2]) + ).first() + + if user is not None: + repo = AssignmentRepo(owner=user, assignment=assignment, + repo_url=repo_url, github_username=user.github_username) + db.session.add(repo) + db.session.commit() + esindex('new-repo', repo_url=repo_url, assignment=str(assignment)) return success_response('initial commit') @@ -427,7 +439,7 @@ def public_webhook(): ) # if the github username is not found, create a dangling submission - enqueue_webhook_rpc(submission.id) + enqueue_webhook(submission.id) return success_response('submission accepted') @@ -447,3 +459,178 @@ def public_whoami(): 'classes': get_classes(u.netid), 'assignments': get_assignments(u.netid), }) + + +@public.route('/ide/list') +@log_endpoint('ide-list', lambda: 'ide-list') +@require_user +@json_response +def public_ide_list(): + """ + List all sessions, active and inactive + + :return: + """ + user: User = current_user() + + return success_response({ + 'sessions': theia_list_all(user.id) + }) + + +@public.route('/ide/stop/') +@log_endpoint('stop-theia-session', lambda: 'stop-theia-session') +@require_user +def public_ide_stop(theia_session_id: int) -> Dict[str, str]: + user: User = current_user() + + theia_session: TheiaSession = TheiaSession.query.filter( + TheiaSession.id == theia_session_id, + TheiaSession.owner_id == user.id, + ).first() + if theia_session is None: + return redirect('/ide?error=Can not find session.') + + theia_session.active = False + theia_session.ended = datetime.now() + theia_session.state = 'Ending' + db.session.commit() + + enqueue_ide_stop(theia_session.id) + + return redirect('/ide') + + +@public.route('/ide/poll/') +@log_endpoint('ide-poll-id', lambda: 'ide-poll') +@require_user +@json_response +def public_ide_poll(theia_session_id: int) -> Dict[str, str]: + """ + Slightly cached endpoint for polling for session data. + + :param theia_session_id: + :return: + """ + user: User = current_user() + + session_data = theia_poll_ide(theia_session_id, user.id) + if session_data is None: + return error_response('Can not find session') + + return success_response({ + 'session': session_data + }) + + +@public.route('/ide/redirect-url/') +@log_endpoint('ide-redirect-url', lambda: 'ide-redirect-url') +@require_user +@json_response +def public_ide_redirect_url(theia_session_id: int) -> Dict[str, str]: + """ + Get the redirect url for a given session + + :param theia_session_id: + :return: + """ + user: User = current_user() + + theia_session: TheiaSession = TheiaSession.query.filter( + TheiaSession.id == theia_session_id, + TheiaSession.owner_id == user.id, + ).first() + if theia_session is None: + return error_response('Can not find session') + + return success_response({ + 'redirect': theia_redirect_url(theia_session.id, user.netid) + }) + + +@public.route('/ide/initialize/') +@log_endpoint('ide-initialize', lambda: 'ide-initialize') +@require_user +@load_from_id(Assignment, verify_owner=False) +def public_ide_initialize(assignment: Assignment): + """ + Redirect to theia proxy. + + :param assignment: + :return: + """ + user: User = current_user() + + if not assignment.ide_enabled: + return error_response('Theia not enabled for this assignment.') + + # Check for existing active session + active_session = TheiaSession.query.join(Assignment).filter( + TheiaSession.owner_id == user.id, + TheiaSession.assignment_id == assignment.id, + TheiaSession.active == True, + Assignment.release_date <= datetime.now(), + Assignment.due_date + timedelta(days=7) >= datetime.now(), + ).first() + if active_session is not None: + return theia_redirect(active_session, user) + + if datetime.now() <= assignment.release_date: + return redirect('/ide?error=Assignment has not been released.') + + if assignment.due_date + timedelta(days=3*7) <= datetime.now(): + return redirect('/ide?error=Assignment due date passed over 3 weeks ago.') + + # Make sure we have a repo we can use + repo = AssignmentRepo.query.filter( + AssignmentRepo.owner_id == user.id, + AssignmentRepo.assignment_id == assignment.id, + ).first() + if repo is None: + return redirect('/courses/assignments?error=Please create your assignment repo first.') + + # Create a new session + session = TheiaSession( + owner_id=user.id, + assignment_id=assignment.id, + repo_id=repo.id, + active=True, + state='Initializing', + ) + db.session.add(session) + db.session.commit() + + # Send kube resource initialization rpc job + enqueue_ide_initialize(session.id) + + # Redirect to proxy + return redirect('/ide') + + +@public.route('/repos') +@require_user +@log_endpoint('repos', lambda: 'repos') +@json_response +def public_repos(): + """ + Get all unique repos for a user + + :return: + """ + user: User = current_user() + + repos: List[AssignmentRepo] = AssignmentRepo.query\ + .join(Assignment)\ + .filter(AssignmentRepo.owner_id == user.id)\ + .distinct(AssignmentRepo.repo_url)\ + .order_by(Assignment.release_date.desc())\ + .all() + + return success_response({ + 'repos': [ + repo.data + for repo in repos + ] + }) + + diff --git a/api/anubis/rpc/__init__.py b/api/anubis/rpc/__init__.py index 44deaaf80..e897c7933 100644 --- a/api/anubis/rpc/__init__.py +++ b/api/anubis/rpc/__init__.py @@ -1,19 +1,3 @@ -import logging -import logstash -import time -import os - -from anubis.models import Submission, Config -from kubernetes import client, config - - -def get_logger(): - logger = logging.getLogger('RPC-Worker') - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) - logger.addHandler(logstash.LogstashHandler('logstash', 5000)) - return logger - """ This is where we should implement any and all job function for the redis queue. The rq library requires special namespacing in order to work, @@ -21,92 +5,3 @@ def get_logger(): """ -def test_repo(submission_id: int): - """ - This function should launch the appropriate testing container - for the assignment, passing along the function arguments. - - :param submission_id: submission.id of to test - """ - from anubis.app import create_app - from anubis.utils.redis_queue import enqueue_webhook_rpc - - app = create_app() - logger = get_logger() - - logger.info('Starting submission {}'.format(submission_id), extra={ - 'submission_id': submission_id, - }) - - with app.app_context(): - max_jobs = Config.query.filter(Config.key == "MAX_JOBS").first() - max_jobs = int(max_jobs.value) if max_jobs is not None else 10 - submission = Submission.query.filter( - Submission.id == submission_id).first() - - if submission is None: - logger.error('Unable to find submission rpc.test_repo', extra={ - 'submission_id': submission_id, - }) - return - - logger.debug('Found submission {}'.format(submission_id), extra={'submission': submission.data}) - - config.load_incluster_config() - batch_v1 = client.BatchV1Api() - - active_jobs = batch_v1.list_namespaced_job('anubis') - for job in active_jobs.items: - if job.status.succeeded is not None and job.status.succeeded >= 1: - logging.info('deleting namespaced job {}'.format(job.metadata.name)) - batch_v1.delete_namespaced_job( - job.metadata.name, - job.metadata.namespace, - propagation_policy='Background') - - if len(active_jobs.items) > max_jobs: - logging.info('TOO many jobs - re-enqueue {}'.format(submission_id), extra={'submission_id': submission_id}) - enqueue_webhook_rpc(submission_id) - time.sleep(1) - exit(0) - - container = client.V1Container( - name="pipeline", - image=submission.assignment.pipeline_image, - image_pull_policy=os.environ.get('IMAGE_PULL_POLICY', default='Always'), - env=[ - client.V1EnvVar(name="TOKEN", value=submission.token), - client.V1EnvVar(name="COMMIT", value=submission.commit), - client.V1EnvVar(name="GIT_REPO", value=submission.repo.repo_url), - client.V1EnvVar(name="SUBMISSION_ID", value=str(submission.id)), - client.V1EnvVar(name="GIT_CRED", - value_from=client.V1EnvVarSource( - secret_key_ref=client.V1SecretKeySelector( - name='git', - key='credentials' - ))), - ], - resources=client.V1ResourceRequirements( - limits={'cpu': '2', 'memory': '500Mi'}, - requests={'cpu': '1', 'memory': '250Mi'}) - ) - # Create and configurate a spec section - template = client.V1PodTemplateSpec( - metadata=client.V1ObjectMeta(labels={"app": "submission-pipeline", "role": "submission-pipeline-worker"}), - spec=client.V1PodSpec(restart_policy="Never", containers=[container])) - # Create the specification of deployment - spec = client.V1JobSpec( - template=template, - backoff_limit=3, - ttl_seconds_after_finished=30) - # Instantiate the job object - job = client.V1Job( - api_version="batch/v1", - kind="Job", - metadata=client.V1ObjectMeta(name='submission-pipeline-{}-{}'.format( - submission.id, int(time.time()))), - spec=spec) - - logging.debug(job.to_str()) - - batch_v1.create_namespaced_job(body=job, namespace='anubis') diff --git a/api/anubis/rpc/batch.py b/api/anubis/rpc/batch.py new file mode 100644 index 000000000..71aac6027 --- /dev/null +++ b/api/anubis/rpc/batch.py @@ -0,0 +1,16 @@ +from anubis.utils.logger import get_logger + + +def rpc_bulk_regrade(submissions): + from anubis.app import create_app + from anubis.utils.data import bulk_regrade_submission + + app = create_app() + logger = get_logger() + + logger.info('bulk regrading {}'.format(submissions), extra={ + 'submission_id': submissions, + }) + + with app.app_context(): + bulk_regrade_submission(submissions) \ No newline at end of file diff --git a/api/anubis/rpc/pipeline.py b/api/anubis/rpc/pipeline.py new file mode 100644 index 000000000..bfbaa7663 --- /dev/null +++ b/api/anubis/rpc/pipeline.py @@ -0,0 +1,144 @@ +import logging +import os +import time + +import kubernetes +from kubernetes import config, client + +from anubis.models import Config, Submission +from anubis.utils.logger import logger + + +def create_pipeline_job_obj(client, submission): + """ + Create pipeline kube api object. + + :param client: + :param submission: + :return: + """ + container = client.V1Container( + name="pipeline", + image=submission.assignment.pipeline_image, + image_pull_policy=os.environ.get('IMAGE_PULL_POLICY', default='Always'), + env=[ + client.V1EnvVar(name="TOKEN", value=submission.token), + client.V1EnvVar(name="COMMIT", value=submission.commit), + client.V1EnvVar(name="GIT_REPO", value=submission.repo.repo_url), + client.V1EnvVar(name="SUBMISSION_ID", value=str(submission.id)), + client.V1EnvVar(name="GIT_CRED", + value_from=client.V1EnvVarSource( + secret_key_ref=client.V1SecretKeySelector( + name='git', + key='credentials' + ))), + ], + resources=client.V1ResourceRequirements( + limits={'cpu': '2', 'memory': '500Mi'}, + requests={'cpu': '500m', 'memory': '250Mi'}, + ) + ) + # Create and configure a spec section + template = client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"app": "submission-pipeline", "role": "submission-pipeline-worker"}), + spec=client.V1PodSpec(restart_policy="Never", containers=[container]) + ) + # Create the specification of deployment + spec = client.V1JobSpec( + template=template, + backoff_limit=3, + ttl_seconds_after_finished=30 + ) + # Instantiate the job object + job = client.V1Job( + api_version="batch/v1", + kind="Job", + metadata=client.V1ObjectMeta( + name='submission-pipeline-{}-{}'.format( + submission.id, int(time.time()) + ) + ), + spec=spec + ) + + return job + + +def cleanup_jobs(batch_v1) -> int: + """ + Runs through all jobs in the namespace. If the job is finished, it will + send a request to the kube api to delete it. Number of active jobs is + returned. + + :param batch_v1: + :return: number of active jobs + """ + jobs = batch_v1.list_namespaced_job('anubis') + active_count = 0 + for job in jobs.items: + if job.status.succeeded is not None and job.status.succeeded >= 1: + logging.info('deleting namespaced job {}'.format(job.metadata.name)) + try: + batch_v1.delete_namespaced_job( + job.metadata.name, + job.metadata.namespace, + propagation_policy='Background') + except kubernetes.client.exceptions.ApiException: + pass + else: + active_count += 1 + + return active_count + + +def test_repo(submission_id: int): + """ + This function should launch the appropriate testing container + for the assignment, passing along the function arguments. + + :param submission_id: submission.id of to test + """ + from anubis.app import create_app + from anubis.utils.redis_queue import enqueue_webhook + + app = create_app() + + logger.info('Starting submission {}'.format(submission_id), extra={ + 'submission_id': submission_id, + }) + + with app.app_context(): + max_jobs = Config.query.filter(Config.key == "MAX_JOBS").first() + max_jobs = int(max_jobs.value) if max_jobs is not None else 10 + submission = Submission.query.filter( + Submission.id == submission_id).first() + + if submission is None: + logger.error('Unable to find submission rpc.test_repo', extra={ + 'submission_id': submission_id, + }) + return + + logger.debug('Found submission {}'.format(submission_id), extra={'submission': submission.data}) + + # Initialize kube client + config.load_incluster_config() + batch_v1 = client.BatchV1Api() + + # Cleanup finished jobs + active_jobs = cleanup_jobs(batch_v1) + + if active_jobs > max_jobs: + logger.info('TOO many jobs - re-enqueue {}'.format(submission_id), extra={'submission_id': submission_id}) + enqueue_webhook(submission_id) + time.sleep(1) + exit(0) + + # Create job object + job = create_pipeline_job_obj(client, submission) + + # Log + logger.debug('creating pipeline job: ' + job.to_str()) + + # Send to kube api + batch_v1.create_namespaced_job(body=job, namespace='anubis') \ No newline at end of file diff --git a/api/anubis/rpc/theia.py b/api/anubis/rpc/theia.py new file mode 100644 index 000000000..8b0d6c38d --- /dev/null +++ b/api/anubis/rpc/theia.py @@ -0,0 +1,421 @@ +import os +import time +from datetime import datetime, timedelta + +from kubernetes import config, client + +from anubis.models import db, Config, TheiaSession +from anubis.utils.logger import logger +from anubis.utils.elastic import esindex + + +def get_theia_pod_name(theia_session: TheiaSession) -> str: + return "theia-{}-{}".format(theia_session.owner.netid, theia_session.id) + + +def create_theia_pod_obj(theia_session: TheiaSession): + name = get_theia_pod_name(theia_session) + + # PVC + volume_name = name + '-volume' + pvc = client.V1PersistentVolumeClaim( + metadata=client.V1ObjectMeta( + name=volume_name, + labels={ + "app": "theia", + "role": "session-storage", + "netid": theia_session.owner.netid, + "session": str(theia_session.id), + } + ), + spec=client.V1PersistentVolumeClaimSpec( + access_modes=['ReadWriteMany'], + volume_mode="Filesystem", + resources=client.V1ResourceRequirements( + requests={ + "storage": "50M", + } + ) + ) + ) + + # Init container + init_container = client.V1Container( + name="theia-init-{}-{}".format(theia_session.owner.netid, theia_session.id), + image="registry.osiris.services/anubis/theia-init:latest", + image_pull_policy=os.environ.get('IMAGE_PULL_POLICY', default='Always'), + env=[ + client.V1EnvVar(name="GIT_REPO", value=theia_session.repo.repo_url), + client.V1EnvVar(name="GIT_CRED", + value_from=client.V1EnvVarSource( + secret_key_ref=client.V1SecretKeySelector( + name='git', + key='credentials' + ))), + ], + volume_mounts=[ + client.V1VolumeMount( + mount_path='/out', + name=volume_name, + ) + ] + ) + + # Theia container + theia_container = client.V1Container( + name="theia", + image="registry.osiris.services/anubis/theia:latest", + image_pull_policy=os.environ.get('IMAGE_PULL_POLICY', default='Always'), + ports=[client.V1ContainerPort(container_port=3000)], + resources=client.V1ResourceRequirements( + limits={'cpu': '2', 'memory': '500Mi'}, + requests={'cpu': '500m', 'memory': '250Mi'}, + ), + volume_mounts=[ + client.V1VolumeMount( + mount_path='/home/project', + name=volume_name, + ) + ] + ) + + # Sidecar container + sidecar_container = client.V1Container( + name="sidecar", + image="registry.osiris.services/anubis/theia-sidecar:latest", + image_pull_policy=os.environ.get('IMAGE_PULL_POLICY', default='Always'), + env=[ + client.V1EnvVar(name="GIT_CRED", + value_from=client.V1EnvVarSource( + secret_key_ref=client.V1SecretKeySelector( + name='git', + key='credentials' + ))), + ], + volume_mounts=[ + client.V1VolumeMount( + mount_path='/home/project', + name=volume_name, + ) + ] + ) + + # Create pod + pod = client.V1Pod( + spec=client.V1PodSpec( + init_containers=[init_container], + containers=[theia_container, sidecar_container], + volumes=[client.V1Volume(name=volume_name)], + dns_policy="None", + dns_config=client.V1PodDNSConfig( + nameservers=['1.1.1.1'] + ) + ), + metadata=client.V1ObjectMeta( + name="theia-{}-{}".format(theia_session.owner.netid, theia_session.id), + labels={ + "app": "theia", + "role": "theia-session", + "netid": theia_session.owner.netid, + "session": str(theia_session.id), + } + ) + ) + + return pod, pvc + + +def initialize_theia_session(theia_session_id: int): + """ + Create the kube resources for a theia session. Will update database entries if necessary. + + :param theia_session_id: + :return: + """ + from anubis.app import create_app + + app = create_app() + config.load_incluster_config() + v1 = client.CoreV1Api() + + logger.info('Initializing theia session {}'.format(theia_session_id), extra={ + 'Initializing theia session': theia_session_id, + }) + + with app.app_context(): + max_ides = Config.query.filter(Config.key == "MAX_IDES").first() + max_ides = int(max_ides.value) if max_ides is not None else 10 + theia_session = TheiaSession.query.filter( + TheiaSession.id == theia_session_id, + ).first() + + if active_theia_pod_count() >= max_ides: + # If there are too many active pods, recycle the job through the queue + logger.info('Maximum IDEs currently running. Re-enqueuing session_id={} initialized request'.format( + theia_session_id + )) + time.sleep(1) + from anubis.utils.redis_queue import enqueue_ide_initialize + enqueue_ide_initialize(theia_session_id) + return + + if theia_session is None: + logger.error('Unable to find theia session rpc.initialize_theia_session', extra={ + 'theia_session_id': theia_session_id, + }) + return + + logger.debug('Found theia_session {}'.format(theia_session_id), extra={'submission': theia_session.data}) + + # Create pod, and pvc object + pod, pvc = create_theia_pod_obj(theia_session) + + # Log + logger.info('creating theia pod: ' + pod.to_str()) + + # Send to kube api + v1.create_namespaced_persistent_volume_claim(namespace='anubis', body=pvc) + v1.create_namespaced_pod(namespace='anubis', body=pod) + + # Wait for it to have started, then update theia_session state + name = get_theia_pod_name(theia_session) + n = 10 + while True: + pod: client.V1Pod = v1.read_namespaced_pod( + name=name, + namespace='anubis', + ) + + if pod.status.phase == 'Pending': + n += 1 + if n > 60: + logger.error('Theia session took too long to initialize. Freeing worker.') + break + + time.sleep(1) + + if pod.status.phase == 'Running': + theia_session.cluster_address = pod.status.pod_ip + theia_session.state = 'Running' + esindex('theia', body={ + 'event': 'session-init', 'session_id': theia_session.id, + 'netid': theia_session.owner.netid, + }) + logger.info('Theia session started {}'.format(name)) + break + + if pod.status.phase == 'Failed': + theia_session.active = False + theia_session.state = 'Failed' + logger.error('Theia session failed {}'.format(name)) + break + + db.session.commit() + + +def reap_theia_session_resources(theia_session_id: int): + """Mark theia session resources for deletion in kube""" + v1 = client.CoreV1Api() + + logger.info('Reaping TheiaSession {}'.format(theia_session_id)) + esindex('theia', body={ + 'event': 'session-pod-delete', 'session_id': theia_session_id, + }) + + # Delete the pods + v1.delete_collection_namespaced_pod( + namespace='anubis', + label_selector='app=theia,role=theia-session,session={}'.format(theia_session_id), + propagation_policy='Background' + ) + + # Delete the pvc + v1.delete_collection_namespaced_persistent_volume_claim( + namespace='anubis', + label_selector='app=theia,role=session-storage,session={}'.format(theia_session_id), + propagation_policy="Background" + ) + + +def list_theia_pods(): + """Get list of active theia session pods""" + v1 = client.CoreV1Api() + + pods = v1.list_namespaced_pod( + namespace='anubis', + label_selector='app=theia,role=theia-session' + ) + + return pods + + +def active_theia_pod_count() -> int: + """Get number of active theia pods""" + + return len(list_theia_pods().items) + + +def check_active_pods(): + """ + Checks that all active Theia Sessions have active pods + + Will mark pods for deletion if they are marked as not active in db. + Will mark db sessions as not active if they do not have a pod. + + :return: + """ + + logger.info('Checking active ActiveTheia sessions') + + # Get active pods and db rows + active_pods = list_theia_pods() + active_db_sessions = TheiaSession.query.filter( + TheiaSession.active == True, + ).all() + + # Build set of active pod session ids + active_pod_ids = set() + for pod in active_pods.items: + active_pod_ids.add(int(pod.metadata.labels['session'])) + + # Build set of active db sesion ids + active_db_ids = set() + for active_db_session in active_db_sessions: + active_db_ids.add(active_db_session.id) + + # Figure out which ones don't match + # and need to be updated. + stale_pods_ids = active_pod_ids - active_db_ids + stale_db_ids = active_db_ids - active_pod_ids + + if len(stale_pods_ids) > 0: + logger.info('Found stale theia pods to reap: {}'.format(str(list(stale_pods_ids)))) + + if len(stale_db_ids) > 0: + logger.info('Found stale theia database entries: {}'.format(str(list(stale_db_ids)))) + + # Reap theia sessions + for stale_pod_id in stale_pods_ids: + reap_theia_session(stale_pod_id) + + # Update database entries + TheiaSession.query.filter( + TheiaSession.id.in_(list(stale_db_ids)), + ).update(active=False) + db.session.commit() + + +def reap_theia_session(theia_session_id: int): + from anubis.app import create_app + + app = create_app() + config.load_incluster_config() + + logger.info('Attempting to reap theia session {}'.format(theia_session_id)) + + with app.app_context(): + theia_session: TheiaSession = TheiaSession.query.filter( + TheiaSession.id == theia_session_id, + ).first() + + if theia_session is None: + logger.error('Could not find theia session {} when attempting to delete'.format(theia_session_id)) + return + + reap_theia_session_resources(theia_session_id) + + if theia_session.active: + theia_session.active = False + theia_session.ended = datetime.now() + + theia_session.state = 'Ended' + db.session.commit() + + +def reap_all_theia_sessions(*_): + from anubis.app import create_app + + app = create_app() + config.load_incluster_config() + + logger.info('Clearing theia sessions') + + with app.app_context(): + theia_sessions = TheiaSession.query.filter( + TheiaSession.active == True, + ).all() + + for n, theia_session in enumerate(theia_sessions): + # Get pod name + name = get_theia_pod_name(theia_session) + + if theia_session.active: + # Log deletion + logger.info('deleting theia session pod: {}'.format(name), + extra={'session': theia_session.data}) + + # Reap kube resources + reap_theia_session_resources(theia_session.id) + + # Update the database row + theia_session.active = False + theia_session.state = 'Ended' + theia_session.ended = datetime.now() + + # Batch commits in size of 5 + if n % 5 == 0: + db.session.commit() + + db.session.commit() + + +def reap_stale_theia_sessions(*_): + from anubis.app import create_app + + app = create_app() + config.load_incluster_config() + v1 = client.CoreV1Api() + + logger.info('Clearing stale theia sessions') + + with app.app_context(): + resp = v1.list_namespaced_pod( + namespace='anubis', + label_selector="app=theia,role=theia-session" + ) + + for n, pod in enumerate(resp.items): + session_id = int(pod.metadata.labels["session"]) + theia_session: TheiaSession = TheiaSession.query.filter( + TheiaSession.id == session_id + ).first() + + # Make sure we have a session to work on + if theia_session is None: + continue + + # If the session is younger than 6 hours old, continue + if datetime.now() <= theia_session.created + timedelta(hours=6): + continue + + # Log deletion + logger.info('deleting theia session pod: {}'.format(session_id), + extra={'session': theia_session.data}) + + # Reap + reap_theia_session_resources(theia_session.id) + + # Update the database row + theia_session.active = False + theia_session.state = 'Ended' + theia_session.ended = datetime.now() + + # Batch commits in size of 5 + if n % 5 == 0: + db.session.commit() + + # Make sure that database entries marked as active have pods + # and pods have active database entries + check_active_pods() + + db.session.commit() diff --git a/api/anubis/utils/auth.py b/api/anubis/utils/auth.py index f55a68588..97c9c1a8c 100644 --- a/api/anubis/utils/auth.py +++ b/api/anubis/utils/auth.py @@ -44,7 +44,7 @@ def current_user() -> Union[User, None]: return load_user(decoded['netid']) -def get_token(netid: str) -> Union[str, None]: +def get_token(netid: str, **extras) -> Union[str, None]: """ Get token for user by netid @@ -63,4 +63,5 @@ def get_token(netid: str) -> Union[str, None]: return jwt.encode({ 'netid': user.netid, 'exp': datetime.utcnow() + timedelta(hours=6), + **extras, }, config.SECRET_KEY, algorithm='HS256').decode() diff --git a/api/anubis/utils/data.py b/api/anubis/utils/data.py index 46d0cf0a2..2979ddc8a 100644 --- a/api/anubis/utils/data.py +++ b/api/anubis/utils/data.py @@ -1,17 +1,22 @@ +from datetime import datetime from email.mime.text import MIMEText +from hashlib import sha256 from json import dumps -from os import environ +from os import environ, urandom from smtplib import SMTP from typing import List, Union, Dict, Tuple -from flask import Response +from flask import Response, redirect +from parse import parse from anubis.config import config -from anubis.models import User, Class_, InClass, Assignment, Submission, AssignmentRepo +from anubis.models import Assignment, AssignmentRepo, AssignedStudentQuestion, Submission +from anubis.models import TheiaSession +from anubis.models import User, Class_, InClass from anubis.models import db -from anubis.utils.auth import load_user +from anubis.utils.auth import load_user, get_token from anubis.utils.cache import cache -from anubis.utils.redis_queue import enqueue_webhook_rpc +from anubis.utils.redis_queue import enqueue_webhook def is_debug() -> bool: @@ -64,6 +69,7 @@ def get_assignments(netid: str, class_name=None) -> Union[List[Dict[str, str]], assignments = Assignment.query.join(Class_).join(InClass).join(User).filter( User.netid == netid, Assignment.hidden == False, + Assignment.release_date <= datetime.now(), *filters ).order_by(Assignment.due_date.desc()).all() @@ -78,7 +84,8 @@ def get_assignments(netid: str, class_name=None) -> Union[List[Dict[str, str]], @cache.memoize(timeout=3, unless=is_debug) -def get_submissions(netid: str, class_name=None, assignment_name=None, assignment_id=None) -> Union[List[Dict[str, str]], None]: +def get_submissions(netid: str, class_name=None, assignment_name=None, assignment_id=None) -> Union[ + List[Dict[str, str]], None]: """ Get all submissions for a given netid. Cache the results. Optionally specify a class_name and / or assignment_name for additional filtering. @@ -114,14 +121,45 @@ def get_submissions(netid: str, class_name=None, assignment_name=None, assignmen return [s.full_data for s in submissions] +@cache.memoize(timeout=60 * 60, unless=is_debug) +def get_assigned_questions(assignment_id: int, user_id: int): + # Get assigned questions + assigned_questions = AssignedStudentQuestion.query.filter( + AssignedStudentQuestion.assignment_id == assignment_id, + AssignedStudentQuestion.owner_id == user_id, + ).all() + + return [ + assigned_question.data + for assigned_question in assigned_questions + ] + + +def bulk_regrade_submission(submissions): + """ + Regrade a batch of submissions + :param submissions: + :return: + """ + response = [] + for submission in submissions: + response.append(regrade_submission(submission)) + return response + + def regrade_submission(submission): """ Regrade a submission - :param submission: Submissions + :param submission: Union[Submissions, int] :return: dict response """ + if isinstance(submission, int): + submission = Submission.query.filter_by(id=submission).first() + if submission is None: + return error_response('could not find submission') + if not submission.processed: return error_response('submission currently being processed') @@ -129,7 +167,7 @@ def regrade_submission(submission): submission.state = 'Waiting for resources...' submission.init_submission_models() - enqueue_webhook_rpc(submission.id) + enqueue_webhook(submission.id) return success_response({'message': 'regrade started'}) @@ -247,7 +285,7 @@ def fix_dangling(): db.session.add(s) db.session.commit() fixed.append(s.data) - enqueue_webhook_rpc(s.id) + enqueue_webhook(s.id) dangling_submissions = Submission.query.filter( Submission.owner_id == None @@ -270,7 +308,7 @@ def fix_dangling(): db.session.add(s) db.session.commit() fixed.append(s.data) - enqueue_webhook_rpc(s.id) + enqueue_webhook(s.id) return fixed @@ -283,7 +321,7 @@ def stats_for(student_id, assignment_id): Submission.assignment_id == assignment_id, Submission.owner_id == student_id, Submission.processed == True, - ).order_by(Submission.last_updated.desc()).all(): + ).order_by(Submission.created.desc()).all(): correct_count = sum(map(lambda result: 1 if result.passed else 0, submission.test_results)) if correct_count >= best_count: @@ -292,14 +330,14 @@ def stats_for(student_id, assignment_id): return best.id if best is not None else None -@cache.cached(timeout=60*60) +@cache.cached(timeout=60 * 60) def get_students(class_name: str = 'Intro. to Operating Systems'): return [s.data for s in User.query.join(InClass).join(Class_).filter( Class_.name == class_name, ).all()] -@cache.cached(timeout=60*60) +@cache.cached(timeout=60 * 60) def get_students_in_class(class_id): return [c.data for c in User.query.join(InClass).join(Class_).filter( Class_.id == class_id, @@ -307,7 +345,7 @@ def get_students_in_class(class_id): ).all()] -@cache.memoize(timeout=60*60, unless=is_debug) +@cache.memoize(timeout=60 * 60, unless=is_debug) def bulk_stats(assignment_id, netids=None): bests = {} @@ -328,11 +366,12 @@ def bulk_stats(assignment_id, netids=None): netid = student['netid'] if submission_id is None: # no submission - bests[netid] = 'No successful submissions' + bests[netid] = 'No submission' else: submission = Submission.query.filter( Submission.id == submission_id ).first() + repo_path = parse('https://github.com/{}', submission.repo.repo_url)[0] best_count = sum(map(lambda x: 1 if x.passed else 0, submission.test_results)) late = 'past due' if assignment.due_date < submission.created else False late = 'past grace' if assignment.grace_date < submission.created else late @@ -342,14 +381,10 @@ def bulk_stats(assignment_id, netids=None): 'test_results': [submission_test_result.stat_data for submission_test_result in submission.test_results], 'total_tests_passed': best_count, - 'repo_url': submission.repo.repo_url, - 'master': 'https://github.com/{}'.format( - submission.repo.repo_url[submission.repo.repo_url.index(':') + 1:-len('.git')], - ), - 'commit_tree': 'https://github.com/{}/tree/{}'.format( - submission.repo.repo_url[submission.repo.repo_url.index(':') + 1:-len('.git')], - submission.commit - ), + 'full_stats': 'https://anubis.osiris.services/api/private/submission/{}'.format(submission.id), + 'master': 'https://github.com/{}'.format(repo_path), + 'commits': 'https://github.com/{}/commits/master'.format(repo_path), + 'commit_tree': 'https://github.com/{}/tree/{}'.format(repo_path, submission.commit), 'late': late } @@ -482,3 +517,62 @@ def _verify_data_shape(data, shape, path=None) -> Tuple[bool, Union[str, None]]: return True, None + +def split_chunks(lst, n): + """ + Split lst into list of lists size n. + + :return: list of lists + """ + _chunks = [] + for i in range(0, len(lst), n): + _chunks.append(lst[i:i + n]) + return _chunks + + +def rand(): + return sha256(urandom(32)).hexdigest() + + +@cache.memoize(timeout=60*5) +def theia_redirect_url(theia_session_id: int, netid: str) -> str: + """ + Generates the url for redirecting to the theia proxy for the given session. + + :param theia_session_id: + :param netid: + :return: + """ + return "https://{}/initialize?token={}".format( + config.THEIA_DOMAIN, + get_token(netid, session_id=theia_session_id), + ) + + +def theia_redirect(theia_session: TheiaSession, user: User): + return redirect(theia_redirect_url(theia_session.id, user.netid)) + + +@cache.memoize(timeout=1) +def theia_list_all(user_id: int, limit: int = 10): + theia_sessions: List[TheiaSession] = TheiaSession.query.filter( + TheiaSession.owner_id == user_id, + ).order_by(TheiaSession.created.desc()).limit(limit).all() + + return [ + theia_session.data + for theia_session in theia_sessions + ] + + +@cache.memoize(timeout=1) +def theia_poll_ide(theia_session_id: int, user_id: int): + theia_session = TheiaSession.query.filter( + TheiaSession.id == theia_session_id, + TheiaSession.owner_id == user_id, + ).first() + + if theia_session is None: + return None + + return theia_session.data diff --git a/api/anubis/utils/logger.py b/api/anubis/utils/logger.py index bce0d6d09..cc36c1f2a 100644 --- a/api/anubis/utils/logger.py +++ b/api/anubis/utils/logger.py @@ -1,4 +1,19 @@ import logging -import os -logger = logging.getLogger(os.environ.get('LOGGER_NAME', default='anubis-api')) +import logstash + +from anubis.config import config + + +def get_logger(logger_name): + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + + if not config.DISABLE_ELK: + logger.addHandler(logstash.LogstashHandler('logstash', 5000)) + + return logger + + +logger = get_logger(config.LOGGER_NAME) diff --git a/api/anubis/utils/redis_queue.py b/api/anubis/utils/redis_queue.py index 49351d0ad..930105ea0 100644 --- a/api/anubis/utils/redis_queue.py +++ b/api/anubis/utils/redis_queue.py @@ -1,7 +1,8 @@ from redis import Redis from rq import Queue -from anubis.rpc import test_repo +from anubis.rpc.pipeline import test_repo +from anubis.rpc.theia import initialize_theia_session, reap_theia_session, reap_stale_theia_sessions def rpc_enqueue(func, *args): @@ -16,13 +17,26 @@ def rpc_enqueue(func, *args): q.enqueue(func, *args) -def enqueue_webhook_rpc(*args): - """ - Enqueues a test job - - :repo_url str: github repo url (eg https://github.com/os3224/...) - """ +def enqueue_webhook(*args): + """Enqueues a test job""" rpc_enqueue( test_repo, *args ) + + +def enqueue_ide_initialize(*args): + """Enqueue an ide initialization job""" + + rpc_enqueue(initialize_theia_session, *args) + + +def enqueue_ide_stop(*args): + """Reap theia session kube resources""" + + rpc_enqueue(reap_theia_session, *args) + + +def enqueue_ide_reap_stale(*args): + """Reap stale ide resources""" + rpc_enqueue(reap_stale_theia_sessions, *args) \ No newline at end of file diff --git a/api/reaper.py b/api/reaper.py index 38331f02d..31a84cc83 100644 --- a/api/reaper.py +++ b/api/reaper.py @@ -1,8 +1,9 @@ from flask import has_request_context from datetime import datetime, timedelta from anubis.app import create_app -from anubis.models import db, Submission, Assignment +from anubis.models import db, Submission, Assignment, TheiaSession from anubis.utils.data import bulk_stats +from anubis.utils.redis_queue import enqueue_ide_stop, enqueue_ide_reap_stale from sqlalchemy import func, and_ import json @@ -35,7 +36,23 @@ def reap_stale_submissions(): print("") +def reap_theia_sessions(): + # Get theia sessions that are older than n hours + theia_sessions = TheiaSession.query.filter( + TheiaSession.active == True, + TheiaSession.last_proxy <= datetime.now() - timedelta(hours=3), + ).all() + + for theia_session in theia_sessions: + enqueue_ide_stop(theia_session.id) + + def reap_stats(): + """ + Calculate stats for recent submissions + + :return: + """ recent_assignments = Assignment.query.group_by( Assignment.class_id ).having( @@ -59,6 +76,7 @@ def reap(): with app.app_context(): # Reap the stale submissions reap_stale_submissions() + enqueue_ide_reap_stale() with app.test_request_context(): # Calculate bulk stats (pre-process stats calls) diff --git a/api/requirements.txt b/api/requirements.txt index c5701bd3e..559e2a1be 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -2,7 +2,6 @@ flask flask-sqlalchemy pymysql gunicorn -gunicorn[gevent] redis rq docker @@ -19,4 +18,4 @@ flask_limiter pyjwt python-logstash kubernetes - +parse diff --git a/cli/anubis/assignment/.dockerignore b/cli/anubis/assignment/.dockerignore index 764480939..61a58b020 100644 --- a/cli/anubis/assignment/.dockerignore +++ b/cli/anubis/assignment/.dockerignore @@ -1 +1 @@ -sample_assignment \ No newline at end of file +test.sh \ No newline at end of file diff --git a/cli/anubis/assignment/Dockerfile b/cli/anubis/assignment/Dockerfile index debd7251e..6ca066240 100644 --- a/cli/anubis/assignment/Dockerfile +++ b/cli/anubis/assignment/Dockerfile @@ -1,6 +1,4 @@ -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive +FROM registry.osiris.services/anubis/assignment-base:latest RUN apt update \ && apt install -y git python3 python3-pip qemu-system-x86 \ @@ -13,5 +11,7 @@ WORKDIR /root/anubis COPY pipeline.py pipeline.py COPY utils.py utils.py +COPY assignment.py assignment.py +COPY meta.yml meta.yml CMD python3 pipeline.py diff --git a/cli/anubis/assignment/sample_assignment/assignment.py b/cli/anubis/assignment/assignment.py similarity index 100% rename from cli/anubis/assignment/sample_assignment/assignment.py rename to cli/anubis/assignment/assignment.py diff --git a/cli/anubis/assignment/build.sh b/cli/anubis/assignment/build.sh deleted file mode 100755 index e4f0f0791..000000000 --- a/cli/anubis/assignment/build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e - -cd $(dirname $(realpath $0)) - -docker build -t registry.osiris.services/anubis/assignment-base:latest . - - -if [ "$1" = "--push" ]; then - docker push registry.osiris.services/anubis/assignment-base:latest -fi diff --git a/cli/anubis/assignment/sample_assignment/assignment.yml b/cli/anubis/assignment/meta.yml similarity index 80% rename from cli/anubis/assignment/sample_assignment/assignment.yml rename to cli/anubis/assignment/meta.yml index af5e56f30..fff0ee2e4 100644 --- a/cli/anubis/assignment/sample_assignment/assignment.yml +++ b/cli/anubis/assignment/meta.yml @@ -19,3 +19,9 @@ assignment: # This description will be shown to the user on the Anubis website. description: | This is a very long description that encompases the entire assignment + + # Uncomment this section if you want to have question pools + # questions: + # - level: 1 + # questions: + # - "What is 3*4?" diff --git a/cli/anubis/assignment/pipeline.py b/cli/anubis/assignment/pipeline.py index 967a6960f..8d7a0a5a1 100755 --- a/cli/anubis/assignment/pipeline.py +++ b/cli/anubis/assignment/pipeline.py @@ -53,7 +53,7 @@ def post(path: str, data: dict, params=None): # Attempt to contact the pipeline API try: res = requests.post( - 'http://anubis-pipeline-api:5000' + path, + 'http://pipeline-api:5000' + path, headers=headers, params=params, json=data, @@ -85,6 +85,7 @@ def report_panic(message: str, traceback: str, ): 'message': message, 'traceback': traceback, } + print(traceback) logging.info('report_error {}'.format(json.dumps(data, indent=2))) post('/pipeline/report/panic/{}'.format(SUBMISSION_ID), data) @@ -157,14 +158,14 @@ def get_assignment_data() -> dict: # Figure out filename assignment_filename = None - for assignment_filename_option in ['assignment.yml', 'assignment.yaml']: + for assignment_filename_option in ['meta.yml', 'meta.yaml']: if os.path.isfile(assignment_filename_option): assignment_filename = assignment_filename_option break # Make sure we figured out the metadata filename if assignment_filename is None: - report_panic('No assignment.yml was found', '') + report_panic('No meta.yml was found', '') exit(0) # Load yaml diff --git a/cli/anubis/assignment/sample_assignment/Dockerfile b/cli/anubis/assignment/sample_assignment/Dockerfile deleted file mode 100644 index 902adecf9..000000000 --- a/cli/anubis/assignment/sample_assignment/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM registry.osiris.services/anubis/assignment-base:latest - -# Switch to the root user to install stuff -# USER root -# RUN apt update \ -# && apt install -y qemu-system-x86 - -COPY assignment.py . -COPY assignment.yml . diff --git a/cli/anubis/assignment/sample_assignment/test.sh b/cli/anubis/assignment/sample_assignment/test.sh deleted file mode 100755 index a127cf9e7..000000000 --- a/cli/anubis/assignment/sample_assignment/test.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -set -e - -cd $(dirname $(realpath $0)) - -./build.sh - -docker run -it \ - -e DEBUG=1 \ - -e TOKEN=test \ - -e COMMIT=2bc7f8d636365402e2d6cc2556ce814c4fcd1489 \ - -e GIT_REPO=https://github.com/juan-punchman/xv6-public.git \ - -e SUBMISSION_ID=1 \ - registry.osiris.services/anubis/assignment/1 $@ - diff --git a/cli/anubis/assignment/sample_assignment/utils.py b/cli/anubis/assignment/sample_assignment/utils.py deleted file mode 100644 index 516e9dd24..000000000 --- a/cli/anubis/assignment/sample_assignment/utils.py +++ /dev/null @@ -1,116 +0,0 @@ -import os -import subprocess -import typing -import logging -import functools - -DEBUG = os.environ.get('DEBUG', default='0') == '1' - -registered_tests = {} -build_function = None - - -class TestResult(object): - def __init__(self): - self.stdout = None - self.message = None - self.passed = None - - def __repr__(self): - return "".format( - self.passed, - self.message, - self.stdout - ) - -class BuildResult(object): - def __init__(self): - self.stdout = None - self.passed = None - - def __repr__(self): - return "".format( - self.passed, - self.stdout - ) - - -class Panic(Exception): - pass - - -def exec_as_student(cmd, timeout=60) -> typing.Tuple[str, int]: - """ - Run a command as the student. Any and all times that student - code is run, it should be done through this function. Any other - way would be incredibly insecure. - - :param cmd: Command to run - :param timeout: Timeout for command - :return: - """ - - if os.getcwd() == '/home/anubis': - os.chdir('./student') - - return_code = 0 - try: - print('{} {}'.format(os.getcwd(), ["su", "student", "-c", cmd])) - stdout = subprocess.check_output( - ["su", "student", "-c", cmd], - timeout=timeout, - stderr=subprocess.STDOUT, - ) - except subprocess.CalledProcessError as e: - stdout = e.output - return_code = e.returncode - - logging.info('exec_as_student command={} return_code={} stdout={}'.format( - cmd, return_code, stdout - )) - fix_permissions() - return stdout, return_code - - -def fix_permissions(): - """ - Fix the file permissions of the student repo - - :return: - """ - # Update file permissions - if os.getcwd() == '/root/anubis': - os.system('chown student:student -R ./student') - elif os.getcwd() == '/root/anubis/student': - os.system('chown student:student -R .') - else: - print('PANIC I DONT KNOW WHERE I AM') - exit(0) - - -def register_test(test_name): - def decorator(func): - @functools.wraps(func) - def wrapper(): - result = TestResult() - func(result) - return result - - registered_tests[test_name] = wrapper - - return wrapper - - return decorator - - -def register_build(func): - @functools.wraps(func) - def wrapper(): - result = BuildResult() - func(result) - return result - - global build_function - build_function = wrapper - - return wrapper diff --git a/cli/anubis/assignment/test.sh b/cli/anubis/assignment/test.sh index a127cf9e7..27d570830 100755 --- a/cli/anubis/assignment/test.sh +++ b/cli/anubis/assignment/test.sh @@ -4,13 +4,8 @@ set -e cd $(dirname $(realpath $0)) -./build.sh - +anubis assignment build docker run -it \ -e DEBUG=1 \ - -e TOKEN=test \ - -e COMMIT=2bc7f8d636365402e2d6cc2556ce814c4fcd1489 \ - -e GIT_REPO=https://github.com/juan-punchman/xv6-public.git \ - -e SUBMISSION_ID=1 \ - registry.osiris.services/anubis/assignment/1 $@ + registry.osiris.services/anubis/assignment/{unique_code} $@ diff --git a/cli/anubis/assignment/utils.py b/cli/anubis/assignment/utils.py index ea4c9b2e7..e5c490345 100644 --- a/cli/anubis/assignment/utils.py +++ b/cli/anubis/assignment/utils.py @@ -1,8 +1,9 @@ +import subprocess import functools import logging -import os -import subprocess import typing +import json +import os DEBUG = os.environ.get('DEBUG', default='0') == '1' @@ -60,7 +61,7 @@ def exec_as_student(cmd, timeout=60) -> typing.Tuple[str, int]: stdout = subprocess.check_output( ["env", "-i", "su", "student", "-c", cmd], timeout=timeout, - # stderr=subprocess.STDOUT, + stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as e: stdout = e.output @@ -115,3 +116,78 @@ def wrapper(): build_function = wrapper return wrapper + + +def trim(stdout: str): + stdout = stdout.split('\n') + try: + stdout = stdout[stdout.index('init: starting sh')+1:] + except ValueError or IndexError: + return stdout + + while len(stdout) != 0 and (len(stdout[-1].strip()) == 0 or stdout[-1].strip() == '$'): + stdout.pop() + + if len(stdout) != 0 and stdout[-1].endswith('$'): + stdout[-1] = stdout[-1].rstrip('$') + + if len(stdout) != 0 and stdout[0].startswith('$'): + stdout[0] = stdout[0].lstrip('$').strip() + + for index in range(len(stdout)): + stdout[index] = stdout[index].strip() + + if len(stdout) != 0 and 'terminating on signal 15' in stdout[-1]: + stdout.pop() + + if len(stdout) != 0: + stdout[-1] = stdout[-1].strip('$') + + print(json.dumps(stdout, indent=2)) + return stdout + + +def verify_expected(stdout, expected): + def test_lines(lines, _expected): + return len(lines) == len(_expected) \ + and all(l.strip() == e.strip() for l, e in zip(lines, _expected)) + + if not test_lines(stdout, expected): + return ( + 'your lines:\n' + '\n'.join(stdout) + '\n\nwe expected:\n' + '\n'.join(expected), + 'Did not recieve exepected output', + False + ) + else: + return ( + 'test passed, we recieved the expected output', + 'Expected output found', + True + ) + + +def xv6_run(cmd): + command = 'timeout 5 qemu-system-i386 -serial mon:stdio ' \ + '-drive file=./xv6.img,media=disk,index=0,format=raw ' \ + '-drive file=./fs.img,media=disk,index=1,format=raw ' \ + '-smp 1 -m 512 -display none -nographic' + + with open('command', 'w') as f: + f.write('\n' + cmd + '\n') + stdout, retcode = exec_as_student(command + ' < command') + stdout = stdout.decode() + stdout = stdout.split('\n') + + boot_line = None + for index, line in enumerate(stdout): + if line.endswith('xv6...'): + boot_line = index + break + + if boot_line is not None: + stdout = stdout[boot_line:] + + stdout = '\n'.join(stdout) + + return trim(stdout), retcode + diff --git a/cli/anubis/cli.py b/cli/anubis/cli.py index a3dca3e7f..50bf11876 100644 --- a/cli/anubis/cli.py +++ b/cli/anubis/cli.py @@ -152,7 +152,7 @@ def assignment(): @assignment.command() def sync(): - assignment_meta = yaml.safe_load(open('assignment.yml').read()) + assignment_meta = yaml.safe_load(open('meta.yml').read()) click.echo(json.dumps(assignment_meta, indent=2)) import assignment import utils @@ -166,20 +166,23 @@ def sync(): def init(assignment_name): safe_assignment_name = safe_filename(assignment_name) + assignment_base: str = os.path.abspath(os.path.join(os.path.dirname(__file__), 'assignment')) + # Copy files over click.echo('Creating assignment directory...') - sample_base = os.path.join(assignment_base, 'sample_assignment') shutil.copytree( - sample_base, + assignment_base, safe_assignment_name, symlinks=True ) click.echo('Initializing the assignment with sample data...') - meta_path = os.path.join(safe_assignment_name, 'assignment.yml') unique_code = base64.b16encode(os.urandom(4)).decode().lower() now = datetime.now() week_from_now = now + timedelta(weeks=1) + + # Populate meta + meta_path = os.path.join(safe_assignment_name, 'meta.yml') meta = open(meta_path).read().format( name=os.path.basename(safe_assignment_name), code=unique_code, @@ -190,6 +193,15 @@ def init(assignment_name): f.write(meta) f.close() + # Populate test.sh + test_path = os.path.join(safe_assignment_name, 'test.sh') + test_sh = open(test_path).read().format( + unique_code=unique_code, + ) + with open(test_path, 'w') as f: + f.write(test_sh) + f.close() + click.echo() click.echo(click.style('You now have an Anubis assignment initialized at ', fg='yellow') + click.style(safe_assignment_name, fg='blue')) @@ -214,7 +226,7 @@ def stats(assignment, netids): @click.argument('path', type=click.Path(exists=True), default='.') @click.option('--push/-p', default=False) def build(path, push): - assignment_meta = yaml.safe_load(open(os.path.join(path, 'assignment.yml')).read()) + assignment_meta = yaml.safe_load(open(os.path.join(path, 'meta.yml')).read()) # Build base image assert os.system('docker build -t {} {}'.format( diff --git a/docker-compose.yml b/docker-compose.yml index f2e5d89c1..fc9a81a82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,7 @@ services: environment: - "DEBUG=1" - "DB_HOST=db" + - "THEIA_DOMAIN=ide.localhost" ports: - 127.0.0.1:5000:5000 volumes: @@ -216,6 +217,21 @@ services: volumes: - "/usr/share/zoneinfo/America/New_York:/etc/localtime:ro" + theia: + image: registry.osiris.services/anubis/theia:latest + build: ./theia/theia + + theia-proxy: + image: registry.osiris.services/anubis/theia-proxy:latest + build: ./theia/proxy + + theia-init: + image: registry.osiris.services/anubis/theia-init:latest + build: ./theia/init + + theia-sidecar: + image: registry.osiris.services/anubis/theia-sidecar:latest + build: ./theia/sidecar volumes: traefik_data: diff --git a/kube/helm/.helmignore b/kube/.helmignore similarity index 100% rename from kube/helm/.helmignore rename to kube/.helmignore diff --git a/kube/helm/Chart.yaml b/kube/Chart.yaml similarity index 100% rename from kube/helm/Chart.yaml rename to kube/Chart.yaml diff --git a/kube/debug-config/dashboard.yml b/kube/debug-config/dashboard.yml deleted file mode 100644 index dea19bb8b..000000000 --- a/kube/debug-config/dashboard.yml +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright 2017 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Namespace -metadata: - name: kubernetes-dashboard - ---- - -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard - namespace: kubernetes-dashboard - ---- - -kind: Service -apiVersion: v1 -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard - namespace: kubernetes-dashboard -spec: - type: NodePort - ports: - - port: 443 - targetPort: 8443 - selector: - k8s-app: kubernetes-dashboard - ---- - -apiVersion: v1 -kind: Secret -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard-certs - namespace: kubernetes-dashboard -type: Opaque - ---- - -apiVersion: v1 -kind: Secret -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard-csrf - namespace: kubernetes-dashboard -type: Opaque -data: - csrf: "" - ---- - -apiVersion: v1 -kind: Secret -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard-key-holder - namespace: kubernetes-dashboard -type: Opaque - ---- - -kind: ConfigMap -apiVersion: v1 -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard-settings - namespace: kubernetes-dashboard - ---- - -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard - namespace: kubernetes-dashboard -rules: - # Allow Dashboard to get, update and delete Dashboard exclusive secrets. - - apiGroups: [""] - resources: ["secrets"] - resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs", "kubernetes-dashboard-csrf"] - verbs: ["get", "update", "delete"] - # Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map. - - apiGroups: [""] - resources: ["configmaps"] - resourceNames: ["kubernetes-dashboard-settings"] - verbs: ["get", "update"] - # Allow Dashboard to get metrics. - - apiGroups: [""] - resources: ["services"] - resourceNames: ["heapster", "dashboard-metrics-scraper"] - verbs: ["proxy"] - - apiGroups: [""] - resources: ["services/proxy"] - resourceNames: ["heapster", "http:heapster:", "https:heapster:", "dashboard-metrics-scraper", "http:dashboard-metrics-scraper"] - verbs: ["get"] - ---- - -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard -rules: - # Allow Metrics Scraper to get metrics from the Metrics server - - apiGroups: ["metrics.k8s.io"] - resources: ["pods", "nodes"] - verbs: ["get", "list", "watch"] - ---- - -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard - namespace: kubernetes-dashboard -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: kubernetes-dashboard -subjects: - - kind: ServiceAccount - name: kubernetes-dashboard - namespace: kubernetes-dashboard - ---- - -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: kubernetes-dashboard -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: kubernetes-dashboard -subjects: - - kind: ServiceAccount - name: kubernetes-dashboard - namespace: kubernetes-dashboard - ---- - -kind: Deployment -apiVersion: apps/v1 -metadata: - labels: - k8s-app: kubernetes-dashboard - name: kubernetes-dashboard - namespace: kubernetes-dashboard -spec: - replicas: 1 - revisionHistoryLimit: 10 - selector: - matchLabels: - k8s-app: kubernetes-dashboard - template: - metadata: - labels: - k8s-app: kubernetes-dashboard - spec: - containers: - - name: kubernetes-dashboard - image: kubernetesui/dashboard:v2.0.3 - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8443 - protocol: TCP - args: - - --auto-generate-certificates - - --namespace=kubernetes-dashboard - # Uncomment the following line to manually specify Kubernetes API server Host - # If not specified, Dashboard will attempt to auto discover the API server and connect - # to it. Uncomment only if the default does not work. - # - --apiserver-host=http://my-address:port - volumeMounts: - - name: kubernetes-dashboard-certs - mountPath: /certs - # Create on-disk volume to store exec logs - - mountPath: /tmp - name: tmp-volume - livenessProbe: - httpGet: - scheme: HTTPS - path: / - port: 8443 - initialDelaySeconds: 30 - timeoutSeconds: 30 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 1001 - runAsGroup: 2001 - volumes: - - name: kubernetes-dashboard-certs - secret: - secretName: kubernetes-dashboard-certs - - name: tmp-volume - emptyDir: {} - serviceAccountName: kubernetes-dashboard - nodeSelector: - "kubernetes.io/os": linux - # Comment the following tolerations if Dashboard must not be deployed on master - tolerations: - - key: node-role.kubernetes.io/master - effect: NoSchedule - ---- - -kind: Service -apiVersion: v1 -metadata: - labels: - k8s-app: dashboard-metrics-scraper - name: dashboard-metrics-scraper - namespace: kubernetes-dashboard -spec: - ports: - - port: 8000 - targetPort: 8000 - selector: - k8s-app: dashboard-metrics-scraper - ---- - -kind: Deployment -apiVersion: apps/v1 -metadata: - labels: - k8s-app: dashboard-metrics-scraper - name: dashboard-metrics-scraper - namespace: kubernetes-dashboard -spec: - replicas: 1 - revisionHistoryLimit: 10 - selector: - matchLabels: - k8s-app: dashboard-metrics-scraper - template: - metadata: - labels: - k8s-app: dashboard-metrics-scraper - annotations: - seccomp.security.alpha.kubernetes.io/pod: 'runtime/default' - spec: - containers: - - name: dashboard-metrics-scraper - image: kubernetesui/metrics-scraper:v1.0.4 - ports: - - containerPort: 8000 - protocol: TCP - livenessProbe: - httpGet: - scheme: HTTP - path: / - port: 8000 - initialDelaySeconds: 30 - timeoutSeconds: 30 - volumeMounts: - - mountPath: /tmp - name: tmp-volume - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 1001 - runAsGroup: 2001 - serviceAccountName: kubernetes-dashboard - nodeSelector: - "kubernetes.io/os": linux - # Comment the following tolerations if Dashboard must not be deployed on master - tolerations: - - key: node-role.kubernetes.io/master - effect: NoSchedule - volumes: - - name: tmp-volume - emptyDir: {} - ---- - -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: admin-user -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: -- kind: ServiceAccount - name: admin-user - namespace: kubernetes-dashboard - ---- - -apiVersion: v1 -kind: ServiceAccount -metadata: - name: admin-user - namespace: kubernetes-dashboard diff --git a/kube/debug-config/nfs-provisioner/.helmignore b/kube/debug-config/nfs-provisioner/.helmignore deleted file mode 100644 index f0c131944..000000000 --- a/kube/debug-config/nfs-provisioner/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/kube/debug-config/nfs-provisioner/Chart.yaml b/kube/debug-config/nfs-provisioner/Chart.yaml deleted file mode 100644 index c85c3313a..000000000 --- a/kube/debug-config/nfs-provisioner/Chart.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -appVersion: 2.2.1-k8s1.12 -description: nfs-server-provisioner is an out-of-tree dynamic provisioner for Kubernetes. You can use it to quickly & easily deploy shared storage that works almost anywhere. -name: nfs-provisioner -icon: file://../nfs-logo.png -version: 0.3.0 -maintainers: -- name: kiall - email: kiall@macinnes.ie -home: https://github.com/kubernetes/charts/tree/master/stable/nfs-server-provisioner -sources: -- https://github.com/kubernetes-incubator/external-storage/tree/master/nfs -keywords: -- nfs -- storage diff --git a/kube/debug-config/nfs-provisioner/OWNERS b/kube/debug-config/nfs-provisioner/OWNERS deleted file mode 100644 index 9aa78a366..000000000 --- a/kube/debug-config/nfs-provisioner/OWNERS +++ /dev/null @@ -1,4 +0,0 @@ -approvers: -- kiall -reviewers: -- kiall diff --git a/kube/debug-config/nfs-provisioner/README.md b/kube/debug-config/nfs-provisioner/README.md deleted file mode 100644 index 2d6e31d88..000000000 --- a/kube/debug-config/nfs-provisioner/README.md +++ /dev/null @@ -1,172 +0,0 @@ -# NFS Server Provisioner - -[NFS Server Provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs) -is an out-of-tree dynamic provisioner for Kubernetes. You can use it to quickly -& easily deploy shared storage that works almost anywhere. - -This chart will deploy the Kubernetes [external-storage projects](https://github.com/kubernetes-incubator/external-storage) -`nfs` provisioner. This provisioner includes a built in NFS server, and is not intended for connecting to a pre-existing -NFS server. If you have a pre-existing NFS Server, please consider using the [NFS Client Provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client) -instead. - -## Introduction - -This chart bootstraps a [nfs-server-provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs) -deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) -package manager. - -## Configuration - -The following table lists the configurable parameters of the kibana chart and -their default values. - -| Parameter | Description | Default | -|:-------------------------------|:----------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------| -| `imagePullSecrets` | Specify image pull secrets | `nil` (does not add image pull secrets to deployed pods) | -| `image.repository` | The image repository to pull from | `quay.io/kubernetes_incubator/nfs-provisioner` | -| `image.tag` | The image tag to pull from | `v1.0.8` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `service.type` | service type | `ClusterIP` | -| `service.nfsPort` | TCP port on which the nfs-server-provisioner NFS service is exposed | `2049` | -| `service.mountdPort` | TCP port on which the nfs-server-provisioner mountd service is exposed | `20048` | -| `service.rpcbindPort` | TCP port on which the nfs-server-provisioner RPC service is exposed | `51413` | -| `service.nfsNodePort` | if `service.type` is `NodePort` and this is non-empty, sets the nfs-server-provisioner node port of the NFS service | `nil` | -| `service.mountdNodePort` | if `service.type` is `NodePort` and this is non-empty, sets the nfs-server-provisioner node port of the mountd service | `nil` | -| `service.rpcbindNodePort` | if `service.type` is `NodePort` and this is non-empty, sets the nfs-server-provisioner node port of the RPC service | `nil` | -| `persistence.enabled` | Enable config persistence using PVC | `false` | -| `persistence.storageClass` | PVC Storage Class for config volume | `nil` | -| `persistence.accessMode` | PVC Access Mode for config volume | `ReadWriteOnce` | -| `persistence.size` | PVC Storage Request for config volume | `1Gi` | -| `storageClass.create` | Enable creation of a StorageClass to consume this nfs-server-provisioner instance | `true` | -| `storageClass.provisionerName` | The provisioner name for the storageclass | `cluster.local/{release-name}-{chart-name}` | -| `storageClass.defaultClass` | Whether to set the created StorageClass as the clusters default StorageClass | `false` | -| `storageClass.name` | The name to assign the created StorageClass | `nfs` | -| `storageClass.allowVolumeExpansion` | Allow base storage PCV to be dynamically resizeable (set to null to disable ) | `true | -| `storageClass.parameters` | Parameters for StorageClass | `{}` | -| `storageClass.mountOptions` | Mount options for StorageClass | `[ "vers=4.1", "noatime" ]` | -| `storageClass.reclaimPolicy` | ReclaimPolicy field of the class, which can be either Delete or Retain | `Delete` | -| `resources` | Resource limits for nfs-server-provisioner pod | `{}` | -| `nodeSelector` | Map of node labels for pod assignment | `{}` | -| `tolerations` | List of node taints to tolerate | `[]` | -| `affinity` | Map of node/pod affinities | `{}` | - -```console -$ helm install stable/nfs-server-provisioner --name my-release \ - --set=image.tag=v1.0.8,resources.limits.cpu=200m -``` - -Alternatively, a YAML file that specifies the values for the above parameters -can be provided while installing the chart. For example, - -```console -$ helm install stable/nfs-server-provisioner --name my-release -f values.yaml -``` - -> **Tip**: You can use the default [values.yaml](values.yaml) as an example - -## Persistence - -The nfs-server-provisioner image stores it's configuration data, and importantly, **the dynamic volumes it -manages** `/export` path of the container. - -The chart mounts a [Persistent Volume](http://kubernetes.io/docs/user-guide/persistent-volumes/) -volume at this location. The volume can be created using dynamic volume -provisioning. However, **it is highly recommended** to explicitly specify -a storageclass to use rather than accept the clusters default, or pre-create -a volume for each replica. - -If this chart is deployed with more than 1 replica, `storageClass.defaultClass=true` -and `persistence.storageClass`, then the 2nd+ replica will end up using the 1st -replica to provision storage - which is likely never a desired outcome. - -## Recommended Persistence Configuration Examples - -The following is a recommended configuration example when another storage class -exists to provide persistence: - - persistence: - enabled: true - storageClass: "standard" - size: 200Gi - - storageClass: - defaultClass: true - -On many clusters, the cloud provider integration will create a "standard" storage -class which will create a volume (e.g. a Google Compute Engine Persistent Disk or -Amazon EBS volume) to provide persistence. - ---- - -The following is a recommended configuration example when another storage class -does not exist to provide persistence: - - persistence: - enabled: true - storageClass: "-" - size: 200Gi - - storageClass: - defaultClass: true - -In this configuration, a `PersistentVolume` must be created for each replica -to use. Installing the Helm chart, and then inspecting the `PersistentVolumeClaim`'s -created will provide the necessary names for your `PersistentVolume`'s to bind to. - -An example of the necessary `PersistentVolume`: - - apiVersion: v1 - kind: PersistentVolume - metadata: - name: data-nfs-server-provisioner-0 - spec: - capacity: - storage: 200Gi - accessModes: - - ReadWriteOnce - gcePersistentDisk: - fsType: "ext4" - pdName: "data-nfs-server-provisioner-0" - claimRef: - namespace: kube-system - name: data-nfs-server-provisioner-0 - ---- - -The following is a recommended configration example for running on bare metal with a hostPath volume: - - persistence: - enabled: true - storageClass: "-" - size: 200Gi - - storageClass: - defaultClass: true - - nodeSelector: - kubernetes.io/hostname: {node-name} - -In this configuration, a `PersistentVolume` must be created for each replica -to use. Installing the Helm chart, and then inspecting the `PersistentVolumeClaim`'s -created will provide the necessary names for your `PersistentVolume`'s to bind to. - -An example of the necessary `PersistentVolume`: - - apiVersion: v1 - kind: PersistentVolume - metadata: - name: data-nfs-server-provisioner-0 - spec: - capacity: - storage: 200Gi - accessModes: - - ReadWriteOnce - hostPath: - path: /srv/volumes/data-nfs-server-provisioner-0 - claimRef: - namespace: kube-system - name: data-nfs-server-provisioner-0 - -> **Warning**: `hostPath` volumes cannot be migrated between machines by Kubernetes, as such, -in this example, we have restricted the `nfs-server-provisioner` pod to run on a single node. This -is unsuitable for production deployments. diff --git a/kube/debug-config/nfs-provisioner/app-readme.md b/kube/debug-config/nfs-provisioner/app-readme.md deleted file mode 100644 index 8474949f6..000000000 --- a/kube/debug-config/nfs-provisioner/app-readme.md +++ /dev/null @@ -1,12 +0,0 @@ -# NFS Server Provisioner - -[NFS Server Provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs) is an out-of-tree dynamic provisioner for Kubernetes. You can use it to quickly & easily deploy shared storage that works almost anywhere. - -This chart will deploy the Kubernetes [external-storage projects](https://github.com/kubernetes-incubator/external-storage) `nfs-provisioner`. This provisioner includes a built in NFS server, and is not intended for connecting to a pre-existing NFS server. If you have a pre-existing NFS Server, please consider using the [NFS Client Provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client) instead. - -## Tips: -It's recommended to constrain the nfs-provisoner server to run on a particular Node using nodeSelector, you can add nodeSelector using `Edit as YAML`: -``` -nodeSelector: - nfs-provisioner: server # specify your key:value nodeSelector -``` diff --git a/kube/debug-config/nfs-provisioner/questions.yml b/kube/debug-config/nfs-provisioner/questions.yml deleted file mode 100644 index c90a72c1d..000000000 --- a/kube/debug-config/nfs-provisioner/questions.yml +++ /dev/null @@ -1,118 +0,0 @@ -categories: -- storage -labels: - io.rancher.certified: experimental - io.rancher.role: cluster -questions: -- variable: defaultImage - default: true - description: "Use default Docker image" - label: Use Default Image - type: boolean - group: "Container Images" - show_subquestion_if: false - subquestions: - - variable: image.repository - default: "ranchercharts/kubernetes_incubator-nfs-provisioner" - description: "Docker image name" - type: string - label: NFS Image Name - - variable: image.tag - default: "v2.2.1-k8s1.12" - description: "NFS image tag" - type: string - label: Image Tag -- variable: storageClass.create - default: true - description: "Creating the StorageClass" - type: boolean - required: true - label: Creating the StorageClass - group: "StorageClass Setting" - show_subquestion_if: true - subquestions: - - variable: storageClass.name - description: "The name to assign the created StorageClass, default to release name if not set." - type: string - label: StorageClass Name - - variable: storageClass.defaultClass - default: true - description: "Set StorageClass as the default StorageClass" - type: boolean - label: Set StorageClass as the default StorageClass - - variable: storageClass.reclaimPolicy - default: "Delete" - description: "ReclaimPolicy of the Created StorageClass" - required: true - type: enum - label: ReclaimPolicy of the Created StorageClass - options: - - "Delete" - - "Retain" - - variable: storageClass.allowVolumeExpansion - default: true - description: "AllowVolumeExpansion shows whether the storage class allow volume expand" - type: boolean - label: Allow VolumeExpansion -# persistence volName -- variable: persistence.enabled - default: false - description: "Enable persistent volume for the nfs-server-provisioner" - type: boolean - required: true - label: Enable Persistent Volume - show_subquestion_if: true - group: "Persistent Storage" - subquestions: - - variable: persistence.size - default: "20Gi" - description: "nfs-server-provisionner Persistent Volume Size" - type: string - label: nfs-server-provisionner Volume Size - - variable: persistence.storageClass - default: "" - description: "If undefined or null, uses the default StorageClass. Default to null" - type: storageclass - label: Default StorageClass for nfs-server-provisionner -- variable: persistence.hostPath - default: "/srv" - description: "For GKE uses /home/kubernetes/nfs/ instead, custom nfs host path read and write permission are required, default to /srv" - type: string - label: NFS Host Path - required: true - show_if: "persistence.enabled=false" - group: "Persistent Storage" -# nfs service type -- variable: service.type - default: "ClusterIP" - description: "The service type of the nfs-provisioner" - type: enum - label: Service Type - group: "Service Settings" - required: true - options: - - "ClusterIP" - - "NodePort" - show_subquestion_if: "NodePort" - subquestions: - - variable: service.nfsNodePort - default: "" - description: "Sepcify the nodePort of the NFS service" - type: int - label: NodePort of the NFS service - min: 30000 - max: 32767 - - variable: service.mountdNodePort - default: "" - description: "Sepcify the nodePort of the mountd service" - type: int - label: NodePort of the mountd service - min: 30000 - max: 32767 - - variable: service.rpcbindNodePort - default: "" - description: "Sepcify the nodePort of the RPC service" - type: int - label: NodePort of the RCP service - min: 30000 - max: 32767 diff --git a/kube/debug-config/nfs-provisioner/templates/NOTES.txt b/kube/debug-config/nfs-provisioner/templates/NOTES.txt deleted file mode 100644 index 09376d296..000000000 --- a/kube/debug-config/nfs-provisioner/templates/NOTES.txt +++ /dev/null @@ -1,26 +0,0 @@ -The NFS Provisioner service has now been installed. - -{{ if .Values.storageClass.create -}} -A storage class named '{{ .Values.storageClass.name }}' has now been created -and is available to provision dynamic volumes. - -You can use this storageclass by creating a `PersistentVolumeClaim` with the -correct storageClassName attribute. For example: - - --- - kind: PersistentVolumeClaim - apiVersion: v1 - metadata: - name: test-dynamic-volume-claim - spec: - storageClassName: "{{ .Values.storageClass.name }}" - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi - -{{ else -}} -A storage class has NOT been created. You may create a custom `StorageClass` -resource with a `provisioner` attribute of `{{ template "nfs-provisioner.provisionerName" . }}`. -{{ end -}} diff --git a/kube/debug-config/nfs-provisioner/templates/_helpers.tpl b/kube/debug-config/nfs-provisioner/templates/_helpers.tpl deleted file mode 100644 index 75e4b25df..000000000 --- a/kube/debug-config/nfs-provisioner/templates/_helpers.tpl +++ /dev/null @@ -1,54 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "nfs-provisioner.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "nfs-provisioner.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "nfs-provisioner.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "nfs-provisioner.provisionerName" -}} -{{- if .Values.storageClass.provisionerName -}} -{{- printf .Values.storageClass.provisionerName -}} -{{- else -}} -cluster.local/{{ template "nfs-provisioner.fullname" . -}} -{{- end -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "nfs-provisioner.storageClass" -}} -{{- if .Values.storageClass.name -}} -{{- printf .Values.storageClass.name -}} -{{- else -}} -{{- template "nfs-provisioner.fullname" . -}} -{{- end -}} -{{- end -}} diff --git a/kube/debug-config/nfs-provisioner/templates/clusterrole.yaml b/kube/debug-config/nfs-provisioner/templates/clusterrole.yaml deleted file mode 100644 index 5affc4bab..000000000 --- a/kube/debug-config/nfs-provisioner/templates/clusterrole.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{ if .Values.rbac.create -}} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ template "nfs-provisioner.fullname" . }} - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -rules: - - apiGroups: [""] - resources: ["persistentvolumes"] - verbs: ["get", "list", "watch", "create", "delete"] - - apiGroups: [""] - resources: ["persistentvolumeclaims"] - verbs: ["get", "list", "watch", "update"] - - apiGroups: ["storage.k8s.io"] - resources: ["storageclasses"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["events"] - verbs: ["list", "watch", "create", "update", "patch"] - - apiGroups: [""] - resources: ["services", "endpoints"] - verbs: ["get"] - - apiGroups: ["extensions"] - resources: ["podsecuritypolicies"] - resourceNames: ["nfs-provisioner"] - verbs: ["use"] - - apiGroups: [""] - resources: ["endpoints"] - verbs: ["get", "list", "watch", "create", "delete", "update", "patch"] -{{- end -}} diff --git a/kube/debug-config/nfs-provisioner/templates/rolebinding.yaml b/kube/debug-config/nfs-provisioner/templates/rolebinding.yaml deleted file mode 100644 index fc88425f9..000000000 --- a/kube/debug-config/nfs-provisioner/templates/rolebinding.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if .Values.rbac.create }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ template "nfs-provisioner.fullname" . }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ template "nfs-provisioner.fullname" . }} -subjects: - - kind: ServiceAccount - name: {{ template "nfs-provisioner.fullname" . }} - namespace: {{ .Release.Namespace }} -{{- end -}} diff --git a/kube/debug-config/nfs-provisioner/templates/service.yaml b/kube/debug-config/nfs-provisioner/templates/service.yaml deleted file mode 100644 index b2d299e28..000000000 --- a/kube/debug-config/nfs-provisioner/templates/service.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "nfs-provisioner.fullname" . }} - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.nfsPort }} - targetPort: nfs - protocol: TCP - name: nfs -{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nfsNodePort))) }} - nodePort: {{ .Values.service.nfsNodePort }} -{{- end }} - - port: {{ .Values.service.mountdPort }} - targetPort: mountd - protocol: TCP - name: mountd -{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.mountdNodePort))) }} - nodePort: {{ .Values.service.mountdNodePort }} -{{- end }} - - port: {{ .Values.service.rpcbindPort }} - targetPort: rpcbind-tcp - protocol: TCP - name: rpcbind-tcp -{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.rpcbindNodePort))) }} - nodePort: {{ .Values.service.rpcbindNodePort }} -{{- end }} - - port: {{ .Values.service.rpcbindPort }} - targetPort: rpcbind-udp - protocol: UDP - name: rpcbind-udp -{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.rpcbindNodePort))) }} - nodePort: {{ .Values.service.rpcbindNodePort }} -{{- end }} -{{- if .Values.service.externalIPs }} - externalIPs: -{{ toYaml .Values.service.externalIPs | indent 4 }} -{{- end }} - selector: - app: {{ template "nfs-provisioner.name" . }} - release: {{ .Release.Name }} diff --git a/kube/debug-config/nfs-provisioner/templates/serviceaccount.yaml b/kube/debug-config/nfs-provisioner/templates/serviceaccount.yaml deleted file mode 100644 index 79fd0ae0e..000000000 --- a/kube/debug-config/nfs-provisioner/templates/serviceaccount.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if .Values.rbac.create }} -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ template "nfs-provisioner.fullname" . }} -{{- end -}} diff --git a/kube/debug-config/nfs-provisioner/templates/statefulset.yaml b/kube/debug-config/nfs-provisioner/templates/statefulset.yaml deleted file mode 100644 index 4fa42653a..000000000 --- a/kube/debug-config/nfs-provisioner/templates/statefulset.yaml +++ /dev/null @@ -1,107 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: {{ template "nfs-provisioner.fullname" . }} - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -spec: - # TODO: Investigate how/if nfs-provisioner can be scaled out beyond 1 replica - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - app: {{ template "nfs-provisioner.name" . }} - release: {{ .Release.Name }} - serviceName: {{ template "nfs-provisioner.fullname" . }} - template: - metadata: - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - spec: - # NOTE: This is 10 seconds longer than the default nfs-provisioner --grace-period value of 90sec - terminationGracePeriodSeconds: 100 - serviceAccountName: {{ if .Values.rbac.create }}{{ template "nfs-provisioner.fullname" . }}{{ else }}{{ .Values.rbac.serviceAccountName | quote }}{{ end }} - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: nfs - containerPort: 2049 - protocol: TCP - - name: mountd - containerPort: 20048 - protocol: TCP - - name: rpcbind-tcp - containerPort: 111 - protocol: TCP - - name: rpcbind-udp - containerPort: 111 - protocol: UDP - securityContext: - capabilities: - add: - - DAC_READ_SEARCH - - SYS_RESOURCE - args: - - "-provisioner={{ template "nfs-provisioner.provisionerName" . }}" - env: - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: SERVICE_NAME - value: {{ template "nfs-provisioner.fullname" . }} - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - volumeMounts: - - name: data - mountPath: /export - {{- with .Values.resources }} - resources: -{{ toYaml . | indent 12 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: -{{ toYaml . | indent 8 }} - {{- end }} - -{{- if not .Values.persistence.enabled }} - volumes: - - name: data - hostPath: - path: {{ .Values.persistence.hostPath }} -{{- end }} - -{{- if .Values.persistence.enabled }} - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: [ {{ .Values.persistence.accessMode | quote }} ] - {{- if .Values.persistence.storageClass }} - {{- if (eq "-" .Values.persistence.storageClass) }} - storageClassName: "" - {{- else }} - storageClassName: {{ .Values.persistence.storageClass | quote }} - {{- end }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.size | quote }} -{{- end }} diff --git a/kube/debug-config/nfs-provisioner/templates/storageclass.yaml b/kube/debug-config/nfs-provisioner/templates/storageclass.yaml deleted file mode 100644 index 437559aeb..000000000 --- a/kube/debug-config/nfs-provisioner/templates/storageclass.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{ if .Values.storageClass.create -}} -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: {{ template "nfs-provisioner.storageClass" . }} - labels: - app: {{ template "nfs-provisioner.name" . }} - chart: {{ template "nfs-provisioner.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -{{- if .Values.storageClass.defaultClass }} - annotations: - storageclass.kubernetes.io/is-default-class: "true" -{{- end }} -provisioner: {{ template "nfs-provisioner.provisionerName" . }} -reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }} -{{ if .Values.storageClass.allowVolumeExpansion }} -allowVolumeExpansion: {{ .Values.storageClass.allowVolumeExpansion }} -{{ end }} -{{ end -}} -{{- with .Values.storageClass.parameters }} -parameters: -{{ toYaml . | indent 2 }} -{{- end }} -{{- with .Values.storageClass.mountOptions }} -mountOptions: -{{ toYaml . | indent 2 }} -{{- end }} diff --git a/kube/debug-config/nfs-provisioner/values.yaml b/kube/debug-config/nfs-provisioner/values.yaml deleted file mode 100644 index b10b3ee8e..000000000 --- a/kube/debug-config/nfs-provisioner/values.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Default values for nfs-provisioner. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -# imagePullSecrets: - -image: - # repository: quay.io/kubernetes_incubator/nfs-provisioner - repository: ranchercharts/kubernetes_incubator-nfs-provisioner - tag: v2.2.1-k8s1.12 - pullPolicy: IfNotPresent - -service: - type: ClusterIP - - nfsPort: 2049 - mountdPort: 20048 - rpcbindPort: 51413 - # nfsNodePort: - # mountdNodePort: - # rpcbindNodePort: - - externalIPs: [] - -persistence: - enabled: false - - ## Persistent Volume Storage Class - ## If defined, storageClassName: - ## If set to "-", storageClassName: "", which disables dynamic provisioning - ## If undefined (the default) or set to null, no storageClassName spec is - ## set, choosing the default provisioner. (gp2 on AWS, standard on - ## GKE, AWS & OpenStack) - ## - # storageClass: "-" - - accessMode: ReadWriteOnce - size: 1Gi - hostPath: /srv - -## For creating the StorageClass automatically: -storageClass: - create: true - - ## Set a provisioner name. If unset, a name will be generated. - # provisionerName: - - ## Set StorageClass as the default StorageClass - ## Ignored if storageClass.create is false - defaultClass: false - - ## Set a StorageClass name - ## Ignored if storageClass.create is false - # name: nfs - - # set to null to prevent expansion - allowVolumeExpansion: true - ## StorageClass parameters - parameters: {} - - mountOptions: - - vers=4.1 - - noatime - - ## ReclaimPolicy field of the class, which can be either Delete or Retain - reclaimPolicy: Delete - -## For RBAC support: -rbac: - create: true - - ## Ignored if rbac.create is true - ## - serviceAccountName: default - -resources: {} - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/kube/debug.sh b/kube/debug.sh index f7871b18f..06eadd4ee 100755 --- a/kube/debug.sh +++ b/kube/debug.sh @@ -13,6 +13,7 @@ fi if ! kubectl get secrets -n anubis | grep api &> /dev/null; then kubectl create secret generic api \ --from-literal=database-uri=mysql+pymysql://anubis:anubis@mariadb.mariadb.svc.cluster.local/anubis \ + --from-literal=database-password=anubis \ --from-literal=secret-key=$(head -c10 /dev/urandom | openssl sha1 -hex | awk '{print $2}') \ -n anubis @@ -25,31 +26,49 @@ fi pushd .. -docker-compose build api -docker-compose build --parallel web logstash +docker-compose build --parallel api web logstash static theia-proxy theia-init theia-sidecar if ! docker image ls | awk '{print $1}' | grep 'registry.osiris.services/anubis/api-dev' &>/dev/null; then docker-compose build api-dev fi +if ! docker image ls | awk '{print $1}' | grep -w '^registry.osiris.services/anubis/theia$' &>/dev/null; then + docker-compose build theia +fi popd -../pipeline/build.sh - if helm list -n anubis | grep anubis &> /dev/null; then - helm upgrade anubis ./helm -n anubis \ + helm upgrade anubis . -n anubis \ --set "imagePullPolicy=IfNotPresent" \ --set "elasticsearch.storageClassName=standard" \ --set "debug=true" \ + --set "api.replicas=1" \ + --set "static.replicas=1" \ + --set "web.replicas=1" \ + --set "pipeline_api.replicas=1" \ + --set "rpc_workers.replicas=1" \ + --set "theia.proxy.replicas=1" \ + --set "theia.proxy.domain=ide.localhost" \ + --set "rollingUpdates=false" \ --set "domain=localhost" \ --set "elasticsearch.initContainer=false" $@ else - helm install anubis ./helm -n anubis \ + helm install anubis . -n anubis \ --set "imagePullPolicy=IfNotPresent" \ --set "elasticsearch.storageClassName=standard" \ --set "debug=true" \ + --set "api.replicas=1" \ + --set "static.replicas=1" \ + --set "web.replicas=1" \ + --set "pipeline_api.replicas=1" \ + --set "rpc_workers.replicas=1" \ + --set "theia.proxy.replicas=1" \ + --set "theia.proxy.domain=ide.localhost" \ + --set "rollingUpdates=false" \ --set "domain=localhost" \ --set "elasticsearch.initContainer=false" $@ fi -kubectl rollout restart deployments.apps/anubis-api -n anubis -kubectl rollout restart deployments.apps/anubis-pipeline-api -n anubis -kubectl rollout restart deployments.apps/anubis-rpc-workers -n anubis +kubectl rollout restart deployments.apps/api -n anubis +kubectl rollout restart deployments.apps/web -n anubis +kubectl rollout restart deployments.apps/pipeline-api -n anubis +kubectl rollout restart deployments.apps/rpc-workers -n anubis +kubectl rollout restart deployments.apps/theia-proxy -n anubis diff --git a/kube/deploy.sh b/kube/deploy.sh index 6831ce6f9..decbb74b6 100755 --- a/kube/deploy.sh +++ b/kube/deploy.sh @@ -16,25 +16,25 @@ if ! kubectl get secrets -n anubis | grep api &> /dev/null; then read -s -p "Anubis DB Password: " DB_PASS kubectl create secret generic api \ --from-literal=database-uri=mysql+pymysql://anubis:${DB_PASS}@mariadb.mariadb.svc.cluster.local/anubis \ + --from-literal=database-password=${DB_PASS} \ --from-literal=secret-key=$(head -c10 /dev/urandom | openssl sha1 -hex | awk '{print $2}') \ -n anubis fi pushd .. -docker-compose build api -docker-compose build --parallel web logstash static +docker-compose build --parallel api web logstash static theia-proxy theia-init theia-sidecar if ! docker image ls | awk '{print $1}' | grep 'registry.osiris.services/anubis/api-dev' &>/dev/null; then docker-compose build api-dev fi -docker-compose push +if ! docker image ls | awk '{print $1}' | grep -w '^registry.osiris.services/anubis/theia$' &>/dev/null; then + docker-compose build theia +fi +docker-compose push api web logstash static theia-proxy theia-init theia-sidecar popd -# ../pipeline/build.sh --push - - -helm upgrade anubis ./helm -n anubis $@ +helm upgrade anubis . -n anubis $@ # kubectl apply \ # -f config/api.yml \ @@ -45,7 +45,7 @@ helm upgrade anubis ./helm -n anubis $@ # -f config/rpc-workers.yml -# kubectl rollout restart deployments.apps/anubis-api -n anubis -# kubectl rollout restart deployments.apps/anubis-web -n anubis -# kubectl rollout restart deployments.apps/anubis-pipeline-api -n anubis -# kubectl rollout restart deployments.apps/anubis-rpc-workers -n anubis +# kubectl rollout restart deployments.apps/api -n anubis +# kubectl rollout restart deployments.apps/web -n anubis +# kubectl rollout restart deployments.apps/pipeline-api -n anubis +# kubectl rollout restart deployments.apps/rpc-workers -n anubis diff --git a/kube/provision.sh b/kube/provision.sh index 67331bcb3..963ac7baf 100755 --- a/kube/provision.sh +++ b/kube/provision.sh @@ -6,9 +6,14 @@ cd $(dirname $(realpath $0)) echo 'This script will provision your cluster for debugging' +minikube delete if ! minikube status | grep 'kubelet: Running' &> /dev/null; then echo 'staring minikube...' 1>&2 - minikube start --feature-gates=TTLAfterFinished=true + minikube start \ + --feature-gates=TTLAfterFinished=true \ + --ports=80:80,443:443 \ + --network-plugin=cni \ + --cni=calico sleep 1 fi @@ -21,13 +26,7 @@ echo 'Adding traefik ingress label to minikube node...' kubectl label node minikube traefik=ingress --overwrite echo 'Adding traefik resources...' -kubectl apply -f ./debug-config/traefik.yml - - -echo 'Adding nfs' -kubectl create namespace nfs-provisioner -helm install nfs-provisioner ./config/nfs-provisioner --namespace nfs-provisioner - +kubectl apply -f ./traefik.yml helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update @@ -43,3 +42,5 @@ helm install mariadb \ --namespace mariadb \ bitnami/mariadb + +exec ./debug.sh diff --git a/kube/restart.sh b/kube/restart.sh new file mode 100755 index 000000000..9cfc7c813 --- /dev/null +++ b/kube/restart.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +kubectl rollout restart deployments.apps/api -n anubis +kubectl rollout restart deployments.apps/web -n anubis +kubectl rollout restart deployments.apps/pipeline-api -n anubis +kubectl rollout restart deployments.apps/rpc-workers -n anubis +kubectl rollout restart deployments.apps/theia-proxy -n anubis diff --git a/kube/scripts/adminer.sh b/kube/scripts/adminer.sh deleted file mode 100755 index 670f6976a..000000000 --- a/kube/scripts/adminer.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo http://127.0.0.1:5002/ -kubectl port-forward svc/anubis-debug 5002:5002 -n anubis diff --git a/kube/scripts/jupyter.sh b/kube/scripts/jupyter.sh deleted file mode 100755 index 653e396c4..000000000 --- a/kube/scripts/jupyter.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -kubectl logs -l app=anubis-debug -c jupyter -n anubis -kubectl port-forward svc/anubis-debug 5003:5003 -n anubis diff --git a/kube/helm/templates/api.yml b/kube/templates/api.yml similarity index 94% rename from kube/helm/templates/api.yml rename to kube/templates/api.yml index f7eb37a5b..2ce793ba8 100644 --- a/kube/helm/templates/api.yml +++ b/kube/templates/api.yml @@ -1,18 +1,18 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-api + name: api namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} - component: anubis-api + component: api spec: selector: matchLabels: - app: anubis-api + app: api replicas: {{ .Values.api.replicas }} {{- if .Values.api.rollingUpdates }} strategy: @@ -25,13 +25,13 @@ spec: template: metadata: labels: - app: anubis-api + app: api spec: # dnsPolicy: Default # nodeSelector: # datacenter: sdc containers: - - name: anubis-api + - name: api image: {{ .Values.api.image }}:{{ .Values.api.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} ports: @@ -74,6 +74,8 @@ spec: secretKeyRef: name: api key: database-uri + - name: "THEIA_DOMAIN" + value: {{ .Values.theia.proxy.domain | quote }} {{- if .Values.api.healthCheck }} livenessProbe: exec: @@ -92,21 +94,21 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-debug + name: debug namespace: {{ .Release.Namespace }} labels: - app: anubis-debug + app: debug heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: matchLabels: - app: anubis-debug + app: debug template: metadata: labels: - app: anubis-debug + app: debug spec: dnsPolicy: ClusterFirst containers: @@ -153,12 +155,12 @@ metadata: name: anubis namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: - app: anubis-api + app: api ports: - name: web port: 5000 @@ -168,15 +170,15 @@ spec: apiVersion: v1 kind: Service metadata: - name: anubis-debug + name: debug namespace: {{ .Release.Namespace }} labels: - app: anubis-debug + app: debug heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: - app: anubis-debug + app: debug ports: - name: adminer port: 5002 @@ -208,7 +210,7 @@ metadata: name: private-api-basicauth namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} @@ -227,7 +229,7 @@ metadata: name: private-api-basicauth namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} @@ -244,7 +246,7 @@ metadata: name: ingress.route.anubis.api.public namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: @@ -274,7 +276,7 @@ metadata: name: ingress.route.anubis.api.private namespace: {{ .Release.Namespace }} labels: - app: anubis-api + app: api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: diff --git a/kube/helm/templates/elk.yml b/kube/templates/elk.yml similarity index 100% rename from kube/helm/templates/elk.yml rename to kube/templates/elk.yml diff --git a/kube/helm/templates/pipeline-api.yml b/kube/templates/pipeline-api.yml similarity index 94% rename from kube/helm/templates/pipeline-api.yml rename to kube/templates/pipeline-api.yml index 0e4c5e057..fdac2f165 100644 --- a/kube/helm/templates/pipeline-api.yml +++ b/kube/templates/pipeline-api.yml @@ -1,10 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-pipeline-api + name: pipeline-api namespace: {{ .Release.Namespace }} labels: - app: anubis-pipeline-api + app: pipeline-api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} component: anubis-pipline-api @@ -12,7 +12,7 @@ metadata: spec: selector: matchLabels: - app: anubis-pipeline-api + app: pipeline-api replicas: {{ .Values.pipeline_api.replicas }} {{- if .Values.api.rollingUpdates }} strategy: @@ -25,11 +25,11 @@ spec: template: metadata: labels: - app: anubis-pipeline-api + app: pipeline-api spec: dnsPolicy: ClusterFirst containers: - - name: anubis-pipeline-api + - name: pipeline-api image: {{ .Values.api.image }}:{{ .Values.api.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} #resources: @@ -78,16 +78,16 @@ spec: apiVersion: v1 kind: Service metadata: - name: anubis-pipeline-api + name: pipeline-api namespace: {{ .Release.Namespace }} labels: - app: anubis-pipeline-api + app: pipeline-api heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: - app: anubis-pipeline-api + app: pipeline-api ports: - name: web port: 5000 @@ -123,7 +123,7 @@ spec: # Pipeline API - podSelector: matchLabels: - app: anubis-pipeline-api + app: pipeline-api # Github IP Addresses - ipBlock: diff --git a/kube/helm/templates/reaper.yml b/kube/templates/reaper.yml similarity index 94% rename from kube/helm/templates/reaper.yml rename to kube/templates/reaper.yml index 4037a191a..3f9487ea1 100644 --- a/kube/helm/templates/reaper.yml +++ b/kube/templates/reaper.yml @@ -28,6 +28,8 @@ spec: value: {{- if .Values.debug }} "1"{{- else }} "0"{{- end }} - name: "DISABLE_ELK" value: "0" + - name: "LOGGER_NAME" + value: "reaper" - name: DB_HOST value: mariadb.mariadb.svc.cluster.local # sqlalchemy uri diff --git a/kube/helm/templates/redis.yml b/kube/templates/redis.yml similarity index 100% rename from kube/helm/templates/redis.yml rename to kube/templates/redis.yml diff --git a/kube/helm/templates/rpc-workers.yml b/kube/templates/rpc-workers.yml similarity index 89% rename from kube/helm/templates/rpc-workers.yml rename to kube/templates/rpc-workers.yml index 93894ef69..19226822e 100644 --- a/kube/helm/templates/rpc-workers.yml +++ b/kube/templates/rpc-workers.yml @@ -13,7 +13,7 @@ metadata: apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-rpc-workers + name: rpc-workers namespace: {{ .Release.Namespace }} labels: heritage: {{ .Release.Service | quote }} @@ -23,7 +23,7 @@ metadata: spec: selector: matchLabels: - app: anubis-rpc-workers + app: rpc-workers replicas: {{ .Values.rpc_workers.replicas }} {{- if .Values.api.rollingUpdates }} strategy: @@ -36,7 +36,7 @@ spec: template: metadata: labels: - app: anubis-rpc-workers + app: rpc-workers spec: serviceAccountName: pipeline-rpc dnsPolicy: ClusterFirst @@ -72,11 +72,12 @@ metadata: rbac.authorization.k8s.io/pipeline-create-jobs: "true" rules: - apiGroups: ["batch", "extensions"] - # - # at the HTTP level, the name of the resource for accessing Job - # objects is "jobs" resources: ["jobs"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + verbs: ["get", "list", "watch", "create", "delete"] + +- apiGroups: [""] + resources: ["pods", "persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "delete", "deletecollection"] --- kind: RoleBinding diff --git a/kube/helm/templates/static.yml b/kube/templates/static.yml similarity index 89% rename from kube/helm/templates/static.yml rename to kube/templates/static.yml index 415440c43..2f78c94bf 100644 --- a/kube/helm/templates/static.yml +++ b/kube/templates/static.yml @@ -1,10 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-static + name: static namespace: {{ .Release.Namespace }} labels: - app: anubis-static + app: static heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} component: nginx @@ -12,7 +12,7 @@ metadata: spec: selector: matchLabels: - app: anubis-static + app: static replicas: {{ .Values.static.replicas }} {{- if .Values.api.rollingUpdates }} strategy: @@ -25,10 +25,10 @@ spec: template: metadata: labels: - app: anubis-static + app: static spec: containers: - - name: anubis-static + - name: static image: {{ .Values.static.image }}:{{ .Values.api.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} ports: @@ -48,15 +48,15 @@ spec: apiVersion: v1 kind: Service metadata: - name: anubis-static + name: static namespace: {{ .Release.Namespace }} labels: - app: anubis-static + app: static heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: - app: anubis-static + app: static ports: - name: web port: 5000 @@ -86,7 +86,7 @@ metadata: name: ingress.route.anubis.static namespace: {{ .Release.Namespace }} labels: - app: anubis-static + app: static heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: @@ -103,7 +103,7 @@ spec: - name: strip-static namespace: {{ .Release.Namespace }} services: - - name: anubis-static + - name: static port: 5000 tls: certResolver: tls diff --git a/kube/templates/theia.yml b/kube/templates/theia.yml new file mode 100644 index 000000000..b9b7be4c9 --- /dev/null +++ b/kube/templates/theia.yml @@ -0,0 +1,224 @@ +{{- if .Values.theia.enable }} + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: theia-proxy + namespace: anubis + labels: + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} + component: theia-proxy + +spec: + replicas: {{ .Values.rpc_workers.replicas }} + {{- if .Values.api.rollingUpdates }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + revisionHistoryLimit: 10 + {{- end }} + selector: + matchLabels: + app: theia + component: proxy + template: + metadata: + labels: + app: theia + role: proxy + component: proxy + spec: + containers: + - name: proxy + image: {{ .Values.theia.proxy.image }}:{{ .Values.theia.proxy.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + ports: + - name: http + containerPort: 5000 + env: + - name: "SECRET_KEY" + valueFrom: + secretKeyRef: + name: api + key: secret-key + - name: "DB_PASSWORD" + valueFrom: + secretKeyRef: + name: api + key: database-password + - name: "DB_HOST" + value: "mariadb.mariadb.svc.cluster.local" + +--- + +apiVersion: v1 +kind: Service +metadata: + name: theia-proxy + namespace: anubis + +spec: + selector: + app: theia + component: proxy + ports: + - name: http-tcp + protocol: TCP + port: 5000 + targetPort: 5000 + - name: http-udp + protocol: UDP + port: 5000 + targetPort: 5000 + + +--- + +# Public Ingress Route /api/public/* +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: ingress.route.theia.public + namespace: anubis + labels: + app: theia + +spec: + entryPoints: + - websecure + routes: + - kind: Rule + match: Host(`{{ .Values.theia.proxy.domain }}`) + services: + - name: theia-proxy + port: 5000 + tls: + certResolver: tls + + +--- + +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: theia-session-network-policy + namespace: {{ .Release.Namespace }} + labels: + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} +spec: + podSelector: + matchLabels: + app: theia + role: theia-session + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: theia + role: proxy + + ports: + - protocol: TCP + port: 5000 + - protocol: UDP + port: 5000 + egress: + - to: + - ipBlock: + cidr: 1.1.1.1/32 + + # Github IP Addresses + - ipBlock: + cidr: 192.30.252.0/22 + - ipBlock: + cidr: 185.199.108.0/22 + - ipBlock: + cidr: 140.82.112.0/20 + - ipBlock: + cidr: 13.114.40.48/32 + - ipBlock: + cidr: 52.192.72.89/32 + - ipBlock: + cidr: 52.69.186.44/32 + - ipBlock: + cidr: 15.164.81.167/32 + - ipBlock: + cidr: 52.78.231.108/32 + - ipBlock: + cidr: 13.234.176.102/32 + - ipBlock: + cidr: 13.234.210.38/32 + - ipBlock: + cidr: 13.229.188.59/32 + - ipBlock: + cidr: 13.250.177.223/32 + - ipBlock: + cidr: 52.74.223.119/32 + - ipBlock: + cidr: 13.236.229.21/32 + - ipBlock: + cidr: 13.237.44.5/32 + - ipBlock: + cidr: 52.64.108.95/32 + - ipBlock: + cidr: 18.228.52.138/32 + - ipBlock: + cidr: 18.228.67.229/32 + - ipBlock: + cidr: 18.231.5.6/32 + - ipBlock: + cidr: 18.181.13.223/32 + - ipBlock: + cidr: 54.238.117.237/32 + - ipBlock: + cidr: 54.168.17.15/32 + - ipBlock: + cidr: 3.34.26.58/32 + - ipBlock: + cidr: 13.125.114.27/32 + - ipBlock: + cidr: 3.7.2.84/32 + - ipBlock: + cidr: 3.6.106.81/32 + - ipBlock: + cidr: 18.140.96.234/32 + - ipBlock: + cidr: 18.141.90.153/32 + - ipBlock: + cidr: 18.138.202.180/32 + - ipBlock: + cidr: 52.63.152.235/32 + - ipBlock: + cidr: 3.105.147.174/32 + - ipBlock: + cidr: 3.106.158.203/32 + - ipBlock: + cidr: 54.233.131.104/32 + - ipBlock: + cidr: 18.231.104.233/32 + - ipBlock: + cidr: 18.228.167.86/32 + + ports: + + # HTTP + - protocol: TCP + port: 443 + - protocol: TCP + port: 80 + + # DNS + - protocol: TCP + port: 53 + - protocol: UDP + port: 53 + + +{{- end }} diff --git a/kube/helm/templates/web-static.yml b/kube/templates/web.yml similarity index 88% rename from kube/helm/templates/web-static.yml rename to kube/templates/web.yml index 94e7d0f0d..ef3dc5c90 100644 --- a/kube/helm/templates/web-static.yml +++ b/kube/templates/web.yml @@ -1,18 +1,18 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: anubis-web + name: web namespace: {{ .Release.Namespace }} labels: - app: anubis-web + app: web heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: selector: matchLabels: - app: anubis-web - replicas: 2 + app: web + replicas: {{ .Values.web.replicas }} {{- if .Values.rollingUpdates }} strategy: type: RollingUpdate @@ -24,11 +24,11 @@ spec: template: metadata: labels: - app: anubis-web + app: web spec: dnsPolicy: ClusterFirst containers: - - name: anubis-web + - name: web image: {{ .Values.web.image }}:{{ .Values.web.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} ports: @@ -46,17 +46,17 @@ spec: apiVersion: v1 kind: Service metadata: - name: anubis-web + name: web namespace: {{ .Release.Namespace }} labels: - app: anubis-web + app: web heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: type: NodePort selector: - app: anubis-web + app: web ports: - name: web port: 3000 @@ -70,7 +70,7 @@ metadata: name: ingress.route.anubis.web namespace: {{ .Release.Namespace }} labels: - app: anubis-web + app: web heritage: {{ .Release.Service | quote }} release: {{ .Release.Name | quote }} spec: @@ -85,7 +85,7 @@ spec: namespace: traefik {{- end }} services: - - name: anubis-web + - name: web port: 3000 tls: certResolver: tls diff --git a/kube/debug-config/traefik.yml b/kube/traefik.yml similarity index 100% rename from kube/debug-config/traefik.yml rename to kube/traefik.yml diff --git a/kube/helm/values.yaml b/kube/values.yaml similarity index 87% rename from kube/helm/values.yaml rename to kube/values.yaml index 495bbb66e..ce402453d 100644 --- a/kube/helm/values.yaml +++ b/kube/values.yaml @@ -80,7 +80,13 @@ redis: memory: "1Gi" rpc_workers: - replicas: 2 - + replicas: 10 +theia: + enable: true + proxy: + replicas: 3 + domain: "ide.anubis.osiris.services" + image: "registry.osiris.services/anubis/theia-proxy" + tag: "latest" diff --git a/scripts/adminer.sh b/scripts/adminer.sh new file mode 100755 index 000000000..d03fb572a --- /dev/null +++ b/scripts/adminer.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo http://127.0.0.1:5002/ +kubectl port-forward svc/debug 5002:5002 -n anubis diff --git a/kube/scripts/api.sh b/scripts/api.sh similarity index 100% rename from kube/scripts/api.sh rename to scripts/api.sh diff --git a/kube/scripts/dashboard.sh b/scripts/dashboard.sh similarity index 100% rename from kube/scripts/dashboard.sh rename to scripts/dashboard.sh diff --git a/scripts/geofix.py b/scripts/geofix.py deleted file mode 100755 index 43dbc1c4c..000000000 --- a/scripts/geofix.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/python3 - -import sys -import json -import time -from elasticsearch import Elasticsearch - -es=Elasticsearch(sys.argv[1]) - -data = es.search(index='request', size=5000) -json.dump(data, open('./dump-{}.json'.format(time.time()), 'w')) - -es.indices.delete(index='request') -#for i in ['request_cli','request_job-request','request_rick-roll', 'request_submission-request']: -for i in ['request']: - es.indices.create(index=i, body={"mappings":{"properties": { - "ip": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "location": { - "type": "geo_point" - }, - "msg": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - }}}) - - -def f(x): - l = x['hits']['hits'][:] - b = [] - for i in l: - y = i['_source'] - y['index'] = i['_index'] + '_' + y['type'] - #if y['location'] is not None: - y['location'] = y['location'][::-1] - b.append(y) - return b - - -for i in data['hits']['hits']: - if 'location' in i['_source'] and i['_source']['location']: - i['_source']['location'] = i['_source']['location'][::-1] - try: - es.index(index='request', body=i['_source']) - except Exception as e: - print(i, e) - diff --git a/scripts/jupyter.sh b/scripts/jupyter.sh new file mode 100755 index 000000000..0aebe01c5 --- /dev/null +++ b/scripts/jupyter.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +kubectl logs -l app=debug -c jupyter -n anubis +kubectl port-forward svc/debug 5003:5003 -n anubis diff --git a/kube/scripts/kibana.sh b/scripts/kibana.sh similarity index 100% rename from kube/scripts/kibana.sh rename to scripts/kibana.sh diff --git a/kube/scripts/mysql.sh b/scripts/mysql.sh similarity index 100% rename from kube/scripts/mysql.sh rename to scripts/mysql.sh diff --git a/theia/README.md b/theia/README.md new file mode 100644 index 000000000..95670543e --- /dev/null +++ b/theia/README.md @@ -0,0 +1,47 @@ +# Theia cpp Docker + +A containerized Theia-based C/C++ demo IDE, including commonly used tools: + +- latest clangd Language Server (nightly build) +- latest stand-alone clang-tidy static analyser (nightly build) +- GDB 8.1 (from Ubuntu repo) +- cmake 3.17.0 + +The included Theia-based IDE application has the following notable features + +- [deprecated][latest][@theia/cpp] Language-server built-in clang-tidy static analyser integration. Will analyse files opened in the IDE's editors and report problems for configured rules. See [README](https://github.com/theia-ide/theia/tree/master/packages/cpp#using-the-clang-tidy-linter) for more details, including related preferences. Note: this LSP client is _deprecated_ and will be replaced once `vscode-clangd` works well with _latest_ Theia +- [next][vscode-clangd] The reference [Language Client for clangd](https://open-vsx.org/extension/llvm-vs-code-extensions/vscode-clangd) +- [@theia/cpp-debug] Basic C/C++ debugging support + +## How to use + +Run on http://localhost:3000 with the current directory as a workspace: + +```bash +docker run --security-opt seccomp=unconfined --init -it -p 3000:3000 -v "$(pwd):/home/project:cached" theiaide/theia-cpp:next +``` + +Options: + +- `--security-opt seccomp=unconfined` enables running without [the default seccomp profile](https://docs.docker.com/engine/security/seccomp/) to allow cpp debugging. Note: anecdotally I am able to debug without this option now. I am not sure what may have changed recently. +- `--init` injects an instance of [tini](https://github.com/krallin/tini) in the container, that will wait-for and reap terminated processes, to avoid leaking PIDs. + +## How to build + +Build image using `next` Theia packages and strip the Theia application to save space (with reduced debuggability) + +```bash +docker build --no-cache --build-arg version=next --build-arg strip=true -t theia-cpp:next . +``` + +Build image using `latest` theia packages (Theia app not stripped) + +```bash +docker build --no-cache --build-arg version=latest --build-arg -t theia-cpp:latest . +``` + +## Additional Information + +#### Build Options: + - `--build-arg strip=true` strips the application to save space but with reduced debuggability. + - `--build-arg LLVM=` version of LLVM tools (clang-tools, clangd and clang-tidy) you prefer (defaults to 12). Please set LLVM=9 to build on ppc64le platform. diff --git a/theia/init/Dockerfile b/theia/init/Dockerfile new file mode 100644 index 000000000..ab7309956 --- /dev/null +++ b/theia/init/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:latest + +# Create out directory +VOLUME /out + +# Only dependency is git +RUN apk --update --no-cache add git + +# Copy entrypoint +COPY entrypoint.sh /entrypoint.sh + +# Set entrypoint +CMD /entrypoint.sh + diff --git a/theia/init/entrypoint.sh b/theia/init/entrypoint.sh new file mode 100755 index 000000000..15f3f29a6 --- /dev/null +++ b/theia/init/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +if [ -n "${GIT_CRED}" ]; then + echo "${GIT_CRED}" > /root/.git-credentials + echo "[credential]" >> /root/.gitconfig + echo " helper = store" >> /root/.gitconfig +fi + +set -x + +# Clone +git clone ${GIT_REPO} /out + +# Fix permissions +chown -R 1001:1001 /out diff --git a/theia/proxy/.dockerignore b/theia/proxy/.dockerignore new file mode 100644 index 000000000..2535f303c --- /dev/null +++ b/theia/proxy/.dockerignore @@ -0,0 +1,2 @@ +node_modules +*.py \ No newline at end of file diff --git a/theia/proxy/.gitignore b/theia/proxy/.gitignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/theia/proxy/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/theia/proxy/Dockerfile b/theia/proxy/Dockerfile new file mode 100644 index 000000000..bbdf6101a --- /dev/null +++ b/theia/proxy/Dockerfile @@ -0,0 +1,8 @@ +FROM node:alpine + +WORKDIR /opt/app +COPY . . +RUN yarn + +ENTRYPOINT ["/usr/local/bin/node"] +CMD ["/opt/app/index.js"] diff --git a/theia/proxy/index.js b/theia/proxy/index.js new file mode 100644 index 000000000..3ba723d63 --- /dev/null +++ b/theia/proxy/index.js @@ -0,0 +1,173 @@ +const http = require("http") +const httpProxy = require("http-proxy"); +const Knex = require("knex"); +const jwt = require("jsonwebtoken"); +const Cookie = require("universal-cookie"); +const Redis = require("redis"); +const urlparse = require("url-parse"); + +const SECRET_KEY = process.env.SECRET_KEY || 'DEBUG'; + +const redis = Redis.createClient({host: "redis"}); + +redis.on("error", function (error) { + console.error(error); +}); + +const knex = Knex({ + client: "mysql", + connection: { + database: "anubis", + user: "anubis", + password: process.env.DB_PASSWORD || "anubis", + host: process.env.DB_HOST || "127.0.0.1", + } +}); + +const proxy = httpProxy.createProxyServer({ + ws: true, +}); + + +const authenticate = token => { + if (token) { + try { + const decoded = jwt.verify(token, SECRET_KEY); + return decoded; + } catch (e) { + console.error('Caught auth error', e); + } + } + + return null; +}; + +const get_session_ip = session_id => { + return new Promise((resolve, reject) => { + knex + .first('cluster_address') + .from('theia_session') + .where('id', session_id) + .then((row) => { + console.log(`cluster_ip ${row.cluster_address}`) + resolve(row.cluster_address) + }); + }) +} + + +const log_req = (req, url) => { + console.log(req.method, (new Date()).toISOString(), url.pathname, url.query); +}; + + +const parse_req = req => { + const url = urlparse(req.url); + const {cookies} = new Cookie(req.headers.cookie); + const query = new URLSearchParams(url.query); + let {token} = cookies; + token = authenticate(token); + return {url, token, query, cookies}; +}; + +const initialize = (req, res, url, query) => { + // Authenticate the token in the http query + const query_token = authenticate(query.get('token')); + if (query_token === null) { + res.writeHead(302, {location: 'https://anubis.osiris.services/error'}); + res.end('redirecting...'); + return; + } + + // Set cookie for ide session & redirect + const signed_token = jwt.sign({ + session_id: query_token.session_id, + }, SECRET_KEY, {expiresIn: '6h'}); + res.writeHead(302, { + location: '/', "Set-Cookie": `token=${signed_token}; Max-Age=${6 * 3600}; HttpOnly` + }) + res.end('redirecting...') +}; + +function changeTimezone(date, ianatz) { + // suppose the date is 12:00 UTC + var invdate = new Date(date.toLocaleString('en-US', { + timeZone: ianatz + })); + + // then invdate will be 07:00 in Toronto + // and the diff is 5 hours + var diff = date.getTime() - invdate.getTime(); + + // so 12:00 in Toronto is 17:00 UTC + return new Date(date.getTime() - diff); // needs to substract + +} + +const updateProxyTime = session_id => { + const now = changeTimezone(new Date(), 'America/New_York'); + knex('theia_session') + .where({id: session_id}) + .update({last_proxy: now}) + .then(() => null); +}; + + +var proxyServer = http.createServer(function (req, res) { + const {url, token, query} = parse_req(req); + log_req(req, url); + + switch (url.pathname) { + case '/initialize': + initialize(req, res, url, query) + return; + + case '/ping': + res.writeHead(200); + res.end('pong'); + return; + + default: + if (token === null) { + res.writeHead(401) + res.end('nah') + return; + } + + // updateProxyTime(token.session_id); + get_session_ip(token.session_id).then((session_ip) => { + proxy.web(req, res, { + target: { + host: session_ip, + port: 5000 + } + }); + }) + } +}); + +proxyServer.on("upgrade", function (req, socket) { + const {token, url} = parse_req(req); + log_req(req, url); + + // updateProxyTime(token.session_id); + get_session_ip(token.session_id).then((session_ip) => { + proxy.ws(req, socket, { + target: { + host: session_ip, + port: 5000 + } + }); + }); +}); + +proxyServer.on("error", function (error) { + console.error(error); +}) + +proxy.on('error', function(error) { + console.error(error); +}) + +console.log("starting at 0.0.0.0:5000"); +proxyServer.listen(5000); diff --git a/theia/proxy/package.json b/theia/proxy/package.json new file mode 100644 index 000000000..a58de612d --- /dev/null +++ b/theia/proxy/package.json @@ -0,0 +1,15 @@ +{ + "name": "proxy", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "http-proxy": "^1.18.1", + "jsonwebtoken": "^8.5.1", + "knex": "^0.21.12", + "mysql": "^2.18.1", + "redis": "^3.0.2", + "universal-cookie": "^4.0.4", + "url-parse": "^1.4.7" + } +} diff --git a/theia/proxy/stress.py b/theia/proxy/stress.py new file mode 100644 index 000000000..1a5239eef --- /dev/null +++ b/theia/proxy/stress.py @@ -0,0 +1,17 @@ +import multiprocessing as mp +import requests +import time +import urllib3 + +urllib3.disable_warnings() +def func(_): + requests.get('https://proxy.localhost/bundle.js', verify=False) + +n=10000 +start = time.time() +with mp.Pool(10) as pool: + pool.map(func, [None]*n) + pool.close() +elapsed = time.time() - start +print('took {:0.2f}s for {} requests'.format(elapsed, n)) +print('{:0.2f} req/s'.format(float(n)/elapsed)) diff --git a/theia/proxy/yarn.lock b/theia/proxy/yarn.lock new file mode 100644 index 000000000..dc8fe4fa0 --- /dev/null +++ b/theia/proxy/yarn.lock @@ -0,0 +1,2175 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@kubernetes/client-node@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.13.0.tgz#fb8e5559dacbfbb12f51ae2f3bfea166a0a60aee" + integrity sha512-gtsf/YG/mdi8BhIFbNA9BXxgwi3X6zYP0TKwqYaMm3hAlv320Awx8SpS1+ZjBVb6P8vRbDgzwApNVH22nDiB+A== + dependencies: + "@types/js-yaml" "^3.12.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/stream-buffers" "^3.0.3" + "@types/tar" "^4.0.3" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "1.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^3.13.1" + jsonpath-plus "^0.19.0" + openid-client "^4.1.1" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.2" + stream-buffers "^3.0.2" + tar "^6.0.2" + tmp-promise "^3.0.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^7.3.1" + +"@panva/asn1.js@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" + integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== + +"@sindresorhus/is@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" + integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== + +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== + +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + +"@types/js-yaml@^3.12.1": + version "3.12.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" + integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== + +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + +"@types/minipass@*": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" + integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "14.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" + integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== + +"@types/node@^10.12.0": + version "10.17.44" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.44.tgz#3945e6b702cb6403f22b779c8ea9e5c3f44ead40" + integrity sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw== + +"@types/request@^2.47.1": + version "2.48.5" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" + integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + +"@types/stream-buffers@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b" + integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ== + dependencies: + "@types/node" "*" + +"@types/tar@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489" + integrity sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + +"@types/tough-cookie@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== + +"@types/underscore@^1.8.9": + version "1.10.24" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.24.tgz#dede004deed3b3f99c4db0bdb9ee21cae25befdd" + integrity sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w== + +"@types/ws@^6.0.1": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" + integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg== + dependencies: + "@types/node" "*" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" + integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= + +array-slice@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" + integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bignumber.js@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" + integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cacheable-lookup@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" + integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== + +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +colorette@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cookie@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +execa@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +fined@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" + integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +flagged-respawn@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" + integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + +follow-redirects@^1.0.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getopts@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" + integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.0, glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +got@^11.8.0: + version "11.8.0" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.0.tgz#be0920c3586b07fd94add3b5b27cb28f49e6545f" + integrity sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.0-beta.5.2" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-core-module@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jose@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.3.tgz#9c931ab3e13e2d16a5b9e6183e60b2fc40a8e1b8" + integrity sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw== + dependencies: + "@panva/asn1.js" "^1.0.0" + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonpath-plus@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" + integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== + +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +keyv@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" + integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + dependencies: + json-buffer "3.0.1" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +knex@^0.21.12: + version "0.21.12" + resolved "https://registry.yarnpkg.com/knex/-/knex-0.21.12.tgz#961bdb484311eb853030f6f49bd5bf9eca89dc51" + integrity sha512-AEyyiTM9p/x/Pb38TPZkvphKPmn8UWxP7MdIphzjAOielOfFFeU6pjP6y3M7UJ7rxrQsCrAYHwdonLQ3l1JCDw== + dependencies: + colorette "1.2.1" + commander "^5.1.0" + debug "4.1.1" + esm "^3.2.25" + getopts "2.2.5" + interpret "^2.2.0" + liftoff "3.1.0" + lodash "^4.17.20" + pg-connection-string "2.3.0" + tarn "^3.0.1" + tildify "2.0.0" + v8flags "^3.2.0" + +liftoff@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" + integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== + dependencies: + extend "^3.0.0" + findup-sync "^3.0.0" + fined "^1.0.1" + flagged-respawn "^1.0.0" + is-plain-object "^2.0.4" + object.map "^1.0.0" + rechoir "^0.6.2" + resolve "^1.1.7" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + +lodash@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +make-iterator@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" + integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== + dependencies: + kind-of "^6.0.2" + +map-cache@^0.2.0, map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +micromatch@^3.0.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mysql@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" + integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== + dependencies: + bignumber.js "9.0.0" + readable-stream "2.3.7" + safe-buffer "5.1.2" + sqlstring "2.3.1" + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" + integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + +object.map@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" + integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + +object.pick@^1.2.0, object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +oidc-token-hash@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" + integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +openid-client@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-4.2.1.tgz#8200c0ab6a3b8e954727dfa790847dc5cb8999c2" + integrity sha512-07eOcJeMH3ZHNvx5DVMZQmy3vZSTQqKSSunbtM1pXb+k5LBPi5hMum1vJCFReXlo4wuLEqZ/OgbsZvXPhbGRtA== + dependencies: + base64url "^3.0.1" + got "^11.8.0" + jose "^2.0.2" + lru-cache "^6.0.0" + make-error "^1.3.6" + object-hash "^2.0.1" + oidc-token-hash "^5.0.0" + p-any "^3.0.0" + +p-any@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9" + integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w== + dependencies: + p-cancelable "^2.0.0" + p-some "^5.0.0" + +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-some@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4" + integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig== + dependencies: + aggregate-error "^3.0.0" + p-cancelable "^2.0.0" + +parse-filepath@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" + integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= + dependencies: + is-absolute "^1.0.0" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= + dependencies: + path-root-regex "^0.1.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pg-connection-string@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6" + integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +readable-stream@2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +redis-commands@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23" + integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +redis@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a" + integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ== + dependencies: + denque "^1.4.1" + redis-commands "^1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-alpn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" + integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.1.6, resolve@^1.1.7: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rfc4648@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sqlstring@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" + integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +tar@^6.0.2: + version "6.0.5" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" + integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tarn@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" + integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== + +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + +tmp-promise@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a" + integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA== + dependencies: + tmp "^0.2.0" + +tmp@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + +underscore@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e" + integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +universal-cookie@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" + integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== + dependencies: + "@types/cookie" "^0.3.3" + cookie "^0.4.0" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +uri-js@^4.2.2: + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8flags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== + dependencies: + homedir-polyfill "^1.0.1" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^1.2.14, which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^7.3.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" + integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/theia/sidecar/Dockerfile b/theia/sidecar/Dockerfile new file mode 100644 index 000000000..bb62165fd --- /dev/null +++ b/theia/sidecar/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:latest + +RUN adduser -D -u 1001 theia \ + && apk add --update --no-cache fcron git busybox-suid \ + && chmod +x /usr/sbin/fcron + +VOLUME /home/project + +COPY entrypoint.sh /entrypoint.sh +COPY autosave.sh /autosave.sh + +CMD /entrypoint.sh + + diff --git a/theia/sidecar/autosave.sh b/theia/sidecar/autosave.sh new file mode 100755 index 000000000..2196b0610 --- /dev/null +++ b/theia/sidecar/autosave.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" +cd ${1:-/home/project} + +echo "Autosaving @ $(date)" + + +git add . +git commit -m "Anubis Cloud IDE Autosave" +git push diff --git a/theia/sidecar/entrypoint.sh b/theia/sidecar/entrypoint.sh new file mode 100755 index 000000000..a020605bb --- /dev/null +++ b/theia/sidecar/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +if [ -n "${GIT_CRED}" ]; then + echo "${GIT_CRED}" > /home/theia/.git-credentials + git config --global credential.store helper + git config --global user.email noreply@anubis.osiris.services + git config --global user.username os3224-robot +fi + +chown -R theia:theia /home/theia + +set -x + +echo "*/5 * * * * /autosave.sh" | crontab -u theia - +exec /usr/sbin/fcron --debug --foreground diff --git a/theia/theia/.dockerignore b/theia/theia/.dockerignore new file mode 100644 index 000000000..f5d5f5d1f --- /dev/null +++ b/theia/theia/.dockerignore @@ -0,0 +1,2 @@ +*.tar.gz +venv diff --git a/theia/theia/Dockerfile b/theia/theia/Dockerfile new file mode 100644 index 000000000..b0954c658 --- /dev/null +++ b/theia/theia/Dockerfile @@ -0,0 +1,166 @@ +FROM ubuntu:18.04 as common + +ARG NODE_VERSION=12.18.3 + +ENV NODE_VERSION=$NODE_VERSION + +# User account +RUN adduser --disabled-password --gecos '' --uid 1001 theia + +# Common deps +RUN apt-get update && \ + apt-get -y install build-essential \ + curl \ + git \ + gpg \ + python \ + wget \ + xz-utils && \ + rm -rf /var/lib/apt/lists/* + +# Install Node.js +# From: https://github.com/nodejs/docker-node/blob/6b8d86d6ad59e0d1e7a94cec2e909cad137a028f/8/Dockerfile + +# gpg keys listed at https://github.com/nodejs/node#release-keys +RUN set -ex \ + && for key in \ + 4ED778F539E3634C779C87C6D7062848A1AB005C \ + B9E2F5981AA6E0CD28160D9FF13993A75599653C \ + 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ + B9AE9905FFD7803F25714661B63B535A4C206CA9 \ + 77984A986EBC2AA786BC0F66B01FBB92821C587A \ + 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ + FD3A5288F042B6850C66B31F09FE44734EB7990E \ + 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \ + C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ + DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ + A48C2BEE680E841632CD4E44F07496B3EB3C1762 \ + ; do \ + gpg --batch --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$key" || \ + gpg --batch --keyserver pool.sks-keyservers.net --recv-keys "$key" || \ + gpg --batch --keyserver pgp.mit.edu --recv-keys "$key" || \ + gpg --batch --keyserver keyserver.pgp.com --recv-keys "$key" || \ + gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" ; \ + done + +RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \ + && case "${dpkgArch##*-}" in \ + amd64) ARCH='x64';; \ + ppc64el) ARCH='ppc64le';; \ + s390x) ARCH='s390x';; \ + arm64) ARCH='arm64';; \ + armhf) ARCH='armv7l';; \ + i386) ARCH='x86';; \ + *) echo "unsupported architecture"; exit 1 ;; \ + esac \ + && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \ + && curl -SLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ + && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ + && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ + && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \ + && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ + && ln -s /usr/local/bin/node /usr/local/bin/nodejs + +FROM common as theia + +ARG GITHUB_TOKEN +# Use "latest" or "next" version for Theia packages +ARG version=latest +# Optionally build a striped Theia application with no map file or .ts sources. +# Makes image ~150MB smaller when enabled +ARG strip=false + +ENV DEBIAN_FRONTEND=noninteractive \ + YARN_VERSION=1.22.5 \ + strip=$strip + +# Install Yarn +# From: https://github.com/nodejs/docker-node/blob/6b8d86d6ad59e0d1e7a94cec2e909cad137a028f/8/Dockerfile + +RUN set -ex \ + && for key in \ + 6A010C5166006599AA17F08146C2130DFD2497F5 \ + ; do \ + gpg --batch --keyserver ipv4.pool.sks-keyservers.net --recv-keys "$key" || \ + gpg --batch --keyserver pool.sks-keyservers.net --recv-keys "$key" || \ + gpg --batch --keyserver pgp.mit.edu --recv-keys "$key" || \ + gpg --batch --keyserver keyserver.pgp.com --recv-keys "$key" || \ + gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" ; \ + done \ + && curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \ + && curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc" \ + && gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \ + && mkdir -p /opt/yarn \ + && tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/yarn --strip-components=1 \ + && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ + && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \ + && rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz + +# Theia application + +WORKDIR /home/theia + +ADD $version.package.json package.json + +RUN if [ "$strip" = "true" ]; then \ +yarn --pure-lockfile && \ + NODE_OPTIONS="--max_old_space_size=4096" yarn theia build && \ + yarn theia download:plugins && \ + yarn --production && \ + yarn autoclean --init && \ + echo *.ts >> .yarnclean && \ + echo *.ts.map >> .yarnclean && \ + echo *.spec.* >> .yarnclean && \ + yarn autoclean --force && \ + yarn cache clean \ +;else \ +yarn --cache-folder ./ycache && rm -rf ./ycache && \ + NODE_OPTIONS="--max_old_space_size=4096" yarn theia build && yarn theia download:plugins \ +;fi + +FROM common + +ARG LLVM=12 +ARG CMAKE_VERSION=3.18.1 + +COPY --from=theia /home/theia /home/theia +WORKDIR /home/theia + +# C/C++ Developer tools + +# Install clangd and clang-tidy from the public LLVM PPA (nightly build / development version) +# And also the GDB debugger from the Ubuntu repos +RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \ + echo "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic main" > /etc/apt/sources.list.d/llvm.list && \ + apt-get update && \ + apt-get install -y \ + gcc-multilib \ + g++-multilib \ + clangd-$LLVM \ + gdb \ + qemu-system-i386 && \ + rm -rf /var/lib/apt/lists/* && \ + ln -s /usr/bin/clangd-$LLVM /usr/bin/clangd + +# Install latest stable CMake +RUN wget "https://github.com/Kitware/CMake/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-Linux-x86_64.sh" && \ + chmod a+x cmake-$CMAKE_VERSION-Linux-x86_64.sh && \ + ./cmake-$CMAKE_VERSION-Linux-x86_64.sh --prefix=/usr/ --skip-license && \ + rm cmake-$CMAKE_VERSION-Linux-x86_64.sh + +RUN echo 'set auto-load safe-path /' > /home/theia/.gdbinit \ + && echo 'source /opt/pwndbg/gdbinit.py' >> /home/theia/.gdbinit \ + && git clone https://github.com/pwndbg/pwndbg.git /opt/pwndbg \ + && cd /opt/pwndbg \ + && ./setup.sh + +RUN chmod g+rw /home && \ + mkdir -p /home/project && \ + chown -R theia:theia /home/theia && \ + chown -R theia:theia /home/project; + +USER theia +ENV SHELL=/bin/bash \ + THEIA_DEFAULT_PLUGINS=local-dir:/home/theia/plugins + +CMD [ "sh", "-c", "env -i THEIA_DEFAULT_PLUGINS=local-dir:/home/theia/plugins SHELL=/bin/bash PATH=/bin:/usr/bin:/usr/local/bin /usr/local/bin/node /home/theia/src-gen/backend/main.js /home/project --hostname=0.0.0.0 --port=5000" ] diff --git a/theia/theia/Makefile b/theia/theia/Makefile new file mode 100644 index 000000000..1a78a03a8 --- /dev/null +++ b/theia/theia/Makefile @@ -0,0 +1,23 @@ + +all: build + +build: + docker build \ + --build-arg version=latest \ + --build-arg strip=true \ + -t anubis-theia . + +run: + docker run \ + -it \ + -p 3000:3000 \ + -v "/home/jc/nyu/os/xv6-public:/home/project" \ + anubis-theia + +debug: + docker run \ + -it \ + -p 3000:3000 \ + -v "/home/jc/nyu/os/xv6-public:/home/project" \ + --entrypoint bash \ + anubis-theia diff --git a/theia/theia/latest.package.json b/theia/theia/latest.package.json new file mode 100644 index 000000000..e66af9345 --- /dev/null +++ b/theia/theia/latest.package.json @@ -0,0 +1,83 @@ +{ + "private": true, + "name": "@theia/cpp-docker-example", + "version": "0.0.1", + "license": "Apache-2.0", + "theia": { + "frontend": { + "config": { + "applicationName": "Anubis IDE", + "preferences": { + "files.enableTrash": false, + "uri": "http://localhost:3000/theia" + } + } + } + }, + "dependencies": { + "@theia/callhierarchy": "latest", + "@theia/console": "latest", + "@theia/core": "latest", + "@theia/cpp-debug": "latest", + "@theia/debug": "latest", + "@theia/editor": "latest", + "@theia/editor-preview": "latest", + "@theia/file-search": "latest", + "@theia/filesystem": "latest", + "@theia/getting-started": "latest", + "@theia/git": "latest", + "@theia/keymaps": "latest", + "@theia/markers": "latest", + "@theia/messages": "latest", + "@theia/metrics": "latest", + "@theia/mini-browser": "latest", + "@theia/monaco": "latest", + "@theia/navigator": "latest", + "@theia/outline-view": "latest", + "@theia/output": "latest", + "@theia/plugin": "latest", + "@theia/plugin-ext": "latest", + "@theia/plugin-ext-vscode": "latest", + "@theia/preferences": "latest", + "@theia/preview": "latest", + "@theia/process": "latest", + "@theia/scm": "latest", + "@theia/search-in-workspace": "latest", + "@theia/task": "latest", + "@theia/terminal": "latest", + "@theia/typehierarchy": "latest", + "@theia/userstorage": "latest", + "@theia/variable-resolver": "latest", + "@theia/vsx-registry": "latest", + "@theia/workspace": "latest" + }, + "devDependencies": { + "@theia/cli": "latest" + }, + "theiaPluginsDir": "plugins", + "theiaPlugins": { + "vscode-builtin-configuration-editing": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/configuration-editing-1.39.1-prel.vsix", + "vscode-builtin-cpp": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/cpp-1.39.1-prel.vsix", + "vscode-builtin-ini": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/ini-1.39.1-prel.vsix", + "vscode-builtin-json": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/json-1.39.1-prel.vsix", + "vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix", + "vscode-builtin-log": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/log-1.39.1-prel.vsix", + "vscode-builtin-make": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/make-1.39.1-prel.vsix", + "vscode-builtin-markdown": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/markdown-1.39.1-prel.vsix", + "vscode-builtin-merge-conflicts": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/merge-conflict-1.39.1-prel.vsix", + "vscode-builtin-theme-defaults": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-defaults-1.39.1-prel.vsix", + "vscode-builtin-theme-kimbie-dark": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-kimbie-dark-1.39.1-prel.vsix", + "vscode-builtin-theme-monokai": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-monokai-1.39.1-prel.vsix", + "vscode-builtin-theme-dimmed": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-monokai-dimmed-1.39.1-prel.vsix", + "vscode-builtin-theme-quietlight": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-quietlight-1.39.1-prel.vsix", + "vscode-builtin-theme-solarized-dark": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-solarized-dark-1.39.1-prel.vsix", + "vscode-builtin-theme-tomorrow-night-blue": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/theme-tomorrow-night-blue-1.39.1-prel.vsix", + "vscode-builtin-icon-theme-seti": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/vscode-theme-seti-1.39.1-prel.vsix", + "vscode-builtin-xml": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/xml-1.39.1-prel.vsix", + "vscode-builtin-yaml": "https://github.com/theia-ide/vscode-builtin-extensions/releases/download/v1.39.1-prel/yaml-1.39.1-prel.vsix", + "vscode-clangd": "https://open-vsx.org/api/llvm-vs-code-extensions/vscode-clangd/0.1.5/file/llvm-vs-code-extensions.vscode-clangd-0.1.5.vsix", + "vscode-editorconfig": "https://github.com/theia-ide/editorconfig-vscode/releases/download/v0.14.4/EditorConfig-0.14.4.vsix", + "plantuml": "https://open-vsx.org/api/jebbs/plantuml/2.13.12/file/jebbs.plantuml-2.13.12.vsix", + "webfreak-debug": "https://open-vsx.org/api/webfreak/debug/0.25.0/file/webfreak.debug-0.25.0.vsix" + } +} diff --git a/web/src/App.jsx b/web/src/App.jsx index db5802e4d..9202ff9fc 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -1,10 +1,11 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import {createMuiTheme, makeStyles, ThemeProvider, withStyles} from '@material-ui/core/styles'; +import {makeStyles, ThemeProvider, withStyles} from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import Typography from '@material-ui/core/Typography'; import Link from '@material-ui/core/Link'; +import Chip from '@material-ui/core/Chip'; import {BrowserRouter as Router, Redirect, Route, Switch} from "react-router-dom"; import {SnackbarProvider} from 'notistack'; import CourseView from './Pages/Courses/View'; @@ -16,7 +17,11 @@ import SubmissionsView from './Pages/Submissions/View'; import GetGithubUsername from "./Pages/GithubUsername/GetGithubUsername"; import About from "./Pages/About/About"; import Questions from "./Pages/Questions/View"; +import IDE from "./Pages/IDE/View"; +import Repos from "./Pages/Repos/View"; import theme from './theme'; +import {useQuery} from './utils'; +import Profile from "./Pages/Profile/View"; const drawerWidth = 240; const useStyles = makeStyles(() => ({ @@ -66,6 +71,11 @@ const useStyles = makeStyles(() => ({ width: "100%", // background: '#eaeff1', }, + chip: { + padding: theme.spacing(1), + width: "100%", + textAlign: "center", + } })); function Copyright() { @@ -83,13 +93,9 @@ function Copyright() { function App() { const classes = useStyles(); + const query = useQuery(); const [open, setOpen] = useState(true); - const handleDrawerClose = () => { - setOpen(false); - }; - const handleDrawerOpen = () => { - setOpen(true); - }; + const [showError, setShowError] = useState(!!query.get('error')); return ( @@ -103,8 +109,8 @@ function App() { setOpen(false)} + onOpen={() => setOpen(true)} />
+ + {showError + ?
+ setShowError(false)} + color="secondary" + /> +
+ : null} + + {/* Courses page */} + {/* Assignments page */} @@ -130,15 +149,38 @@ function App() { + + {/* Theia IDE */} + + + + + {/* Repo view */} + + + + + {/* Profile view */} + + + + + {/* Set github username */} + + {/* About */} + + {/* Authentication */} + + {/* 404 Not Found */}
@@ -146,6 +188,7 @@ function App() {
+
diff --git a/web/src/Navigation/Navigator.jsx b/web/src/Navigation/Navigator.jsx index 690632804..19f603c0e 100644 --- a/web/src/Navigation/Navigator.jsx +++ b/web/src/Navigation/Navigator.jsx @@ -22,7 +22,10 @@ import PublicIcon from "@material-ui/icons/Public"; import AssessmentIcon from '@material-ui/icons/Assessment'; import CloseOutlinedIcon from '@material-ui/icons/CloseOutlined'; import LaunchOutlinedIcon from '@material-ui/icons/LaunchOutlined'; +import CodeOutlinedIcon from '@material-ui/icons/CodeOutlined'; import {Link} from "react-router-dom"; +import GitHubIcon from "@material-ui/icons/GitHub"; +import AccountCircleOutlinedIcon from '@material-ui/icons/AccountCircleOutlined'; const categories = [ @@ -35,10 +38,25 @@ const categories = [ icon: , path: "/courses/assignments" }, + { + id: "Repos", + icon: , + path: '/repos', + }, { id: "Submissions", icon: , path: "/courses/assignments/submissions" + }, + { + id: "Anubis IDE", + icon: , + path: "/ide" + }, + { + id: "Profile", + icon: , + path: "/profile" } ]; diff --git a/web/src/Pages/Assignments/Assignment.jsx b/web/src/Pages/Assignments/Assignment.jsx index 18ad9625a..d36cb00b3 100644 --- a/web/src/Pages/Assignments/Assignment.jsx +++ b/web/src/Pages/Assignments/Assignment.jsx @@ -1,4 +1,5 @@ import React, {useState} from "react"; +import {Link} from "react-router-dom"; import {makeStyles} from "@material-ui/core/styles"; import Card from "@material-ui/core/Card"; import CardActionArea from '@material-ui/core/CardActionArea'; @@ -6,12 +7,13 @@ import CardActions from "@material-ui/core/CardActions"; import CardContent from "@material-ui/core/CardContent"; import Button from "@material-ui/core/Button"; import Typography from "@material-ui/core/Typography"; +import Divider from "@material-ui/core/Divider"; import AlarmIcon from '@material-ui/icons/Alarm'; import EventNoteIcon from '@material-ui/icons/EventNote'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; -import {Link} from "react-router-dom"; -import red from '@material-ui/core/colors/red'; import PublishIcon from '@material-ui/icons/Publish'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import red from '@material-ui/core/colors/red'; import green from '@material-ui/core/colors/green'; import blue from '@material-ui/core/colors/blue'; import grey from '@material-ui/core/colors/grey' @@ -50,16 +52,14 @@ const useStyles = makeStyles((theme) => ({ fontSize: 14, paddingLeft: theme.spacing(1) }, - submitIcon: { + actionList: { display: 'flex', - paddingLeft: theme.spacing(0.5), - + flexDirection: 'column', }, mainTitle: { fontWeight: 600, fontSize: 20, letterspacing: 0.4, - }, })); @@ -78,7 +78,15 @@ const remainingTime = (dueDate) => { } export default function AssignmentCard(props) { - const {courseCode, assignmentNumber, assignmentTitle, assignmentId, dueDate, hasSubmission} = props.assignment; + const { + courseCode, + assignmentNumber, + assignmentTitle, + assignmentId, + dueDate, + hasSubmission, + githubClassroomLink + } = props.assignment; const classes = useStyles(); const [timeLeft] = useState(remainingTime(dueDate)); @@ -94,12 +102,17 @@ export default function AssignmentCard(props) { ); }); + const ideMaxTime = new Date(dueDate); + ideMaxTime.setDate(ideMaxTime.getDate() + 7); + const ideEnabled = new Date() < ideMaxTime; + + const githubLinkEnabled = typeof githubClassroomLink === "string"; return ( + to={`/api/public/ide/initialize/${assignmentId}`}> @@ -136,17 +149,26 @@ export default function AssignmentCard(props) { - -
- - - -
+ + +
); diff --git a/web/src/Pages/Assignments/View.jsx b/web/src/Pages/Assignments/View.jsx index ee5daf4e8..692decfea 100644 --- a/web/src/Pages/Assignments/View.jsx +++ b/web/src/Pages/Assignments/View.jsx @@ -62,10 +62,10 @@ export default function AssignmentView() { if (loading) return ; if (error) return ; - const translateAssignmentData = ({id, name, due_date, course, description, has_submission}, index) => ({ + const translateAssignmentData = ({id, name, due_date, course, description, has_submission, github_classroom_link}, index) => ({ courseCode: course.class_code, assignmentId: id, assignmentTitle: name, dueDate: due_date, hasSubmission: has_submission, assignmentDescription: description, - assignmentNumber: data.assignments.length - index, + assignmentNumber: data.assignments.length - index, githubClassroomLink: github_classroom_link, }); return ( diff --git a/web/src/Pages/IDE/Table.jsx b/web/src/Pages/IDE/Table.jsx new file mode 100644 index 000000000..364f85076 --- /dev/null +++ b/web/src/Pages/IDE/Table.jsx @@ -0,0 +1,151 @@ +import React from "react"; +import {makeStyles} from "@material-ui/core/styles"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import IconButton from "@material-ui/core/IconButton"; +import TableContainer from "@material-ui/core/TableContainer"; +import Paper from "@material-ui/core/Paper"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableCell from "@material-ui/core/TableCell"; +import TableBody from "@material-ui/core/TableBody"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import green from "@material-ui/core/colors/green"; +import red from "@material-ui/core/colors/red"; +import blue from "@material-ui/core/colors/blue"; +import grey from "@material-ui/core/colors/grey"; +import CancelIcon from "@material-ui/icons/Cancel"; +import TableFooter from "@material-ui/core/TableFooter"; +import TablePagination from "@material-ui/core/TablePagination"; +import CodeOutlinedIcon from "@material-ui/icons/CodeOutlined"; +import DeleteForeverOutlinedIcon from '@material-ui/icons/DeleteForeverOutlined'; +import TablePaginationActions from "@material-ui/core/TablePagination/TablePaginationActions"; +import Tooltip from "@material-ui/core/Tooltip"; +import GitHubIcon from "@material-ui/icons/GitHub"; + +const useStyles = makeStyles({ + root: { + flexGrow: 1, + }, + table: { + minWidth: 500, + }, + headerText: { + fontWeight: 600 + }, + commitHashContainer: { + width: 200, + overflow: "hidden", + } +}); + +export default function IDETable({rows, headers}) { + const classes = useStyles() + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + + + + + {headers.map(header => ( + + {header} + + ))} + + + + + {(rowsPerPage > 0 + ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + : rows + ).map(row => ( + + + {row.state} + + + {row.active + ? + : } + + + + + + + + + + {row.state === 'Initializing' + ? + : + + + + + } + + + {row.assignment_name} + + + {row.class_name} + + + + + + + + {row.created} + + + ))} + + {emptyRows > 0 && ( + + + + )} + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/web/src/Pages/IDE/View.jsx b/web/src/Pages/IDE/View.jsx new file mode 100644 index 000000000..821d3aec8 --- /dev/null +++ b/web/src/Pages/IDE/View.jsx @@ -0,0 +1,72 @@ +import React from "react"; +import {makeStyles} from "@material-ui/core/styles"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import {Redirect} from "react-router-dom"; +import useSubscribe from "../../useSubscribe"; +import Grid from "@material-ui/core/Grid"; +import Typography from "@material-ui/core/Typography"; +import IDETable from "./Table"; +import Warning from "./Warning"; + + +const useStyles = makeStyles((theme) => ({ + root: { + flexShrink: 0, + marginLeft: theme.spacing(2.5), + }, + instructions: { + paddingTop: theme.spacing(1), + paddingLeft: theme.spacing(1), + } +})); + + +export default function IDE() { + const classes = useStyles(); + const {loading, error, data} = useSubscribe( + '/api/public/ide/list', + 1000, + _data => new Array(..._data.sessions).every(item => ( + item.state !== 'Initializing' && item.state !== 'Ending' + )), + ); + + if (loading) return ; + if (error) return + + return ( +
+ + + + + Anubis Cloud IDE + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/web/src/Pages/IDE/Warning.jsx b/web/src/Pages/IDE/Warning.jsx new file mode 100644 index 000000000..3fa882aa5 --- /dev/null +++ b/web/src/Pages/IDE/Warning.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import red from "@material-ui/core/colors/red"; + +export default function Warning() { + const [open, setOpen] = React.useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ + + {"Anubis IDE Instructions"} + + + This is an early version of the Anubis Cloud IDE platform. Please excuse any performance or + stability issues while we work out the bugs. + + + Before being able to create a Cloud IDE session, you will need to have created a repo for the assignment + you would like to work on. Refer to the assignment PDF to find the github classroom link. + To create a new session, go to the assignments tab and click the "Launch Cloud IDE" button. You will + be taken back to this table while your Cloud IDE instance is being allocated to you. + + + Your cloud IDE session is temporary. The IDE server you are allocated is being limited in: storage, + networking capabilities, memory resources and cpu resources. When you create a Cloud IDE session, we + clone exactly what is in your github repo into your Cloud IDE filesystem. When we stop and reclaim your + Cloud IDE + resources, we delete the Cloud IDE instance allocated to you. This means that the filesystem for your + Cloud IDE system, including your work will be deleted forever. There is a robot user in the Cloud IDE + instance that will automatically commit and push your work every 5 minutes. You should + not rely on this exclusively. + + + Each session will last for a maximum of 6 hours. After that, the resources running your IDE will be + reclaimed. When that happens, any un-pushed work will be lost. + + +
+ By using this feature of Anubis, you are agreeing to accept the responsibility of making sure your work + is saved. You are also agreeing not to attempt any form of abuse or hacking of any kind on our systems. + We log absolutely everything. +
+
+
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/web/src/Pages/Profile/View.jsx b/web/src/Pages/Profile/View.jsx new file mode 100644 index 000000000..97a8e99ed --- /dev/null +++ b/web/src/Pages/Profile/View.jsx @@ -0,0 +1,97 @@ +import React, {useState} from "react"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Grid from "@material-ui/core/Grid"; +import TextField from "@material-ui/core/TextField"; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from "@material-ui/core/Typography"; +import Button from "@material-ui/core/Button"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import SaveIcon from '@material-ui/icons/Save'; +import {Redirect} from "react-router-dom"; +import axios from "axios"; +import useGet from "../../useGet"; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + }, + card: { + minWidth: 300, + }, + cardContent: { + flexDirection: "column", + display: "flex" + }, + textField: { + margin: theme.spacing(1), + } +})); + + +export default function Profile() { + const classes = useStyles(); + const [_github_username, set_github_username] = useState(null); + const {loading, error, data} = useGet('/api/public/whoami'); + + if (loading) return ; + if (error) return ; + + const {user} = data; + + const github_username = _github_username || user.github_username; + + return ( + + + + + + Anubis profile + + + + set_github_username(e.target.value)} + value={github_username} + variant="outlined" + /> + + {/**/} + {/* }*/} + {/* onClick={() => axios.get(`/api/public/set-github-username?github_username=${github_username}`)}*/} + {/* >*/} + {/* Save*/} + {/* */} + {/**/} + + + + ) +} + diff --git a/web/src/Pages/Questions/View.jsx b/web/src/Pages/Questions/View.jsx index 2b6e01343..49fdf63ab 100644 --- a/web/src/Pages/Questions/View.jsx +++ b/web/src/Pages/Questions/View.jsx @@ -6,7 +6,7 @@ import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import {Redirect} from "react-router-dom"; import Typography from "@material-ui/core/Typography"; -import Divider from "@material-ui/core/Divider"; +import Zoom from "@material-ui/core/Zoom"; import ReactMarkdownWithHtml from "react-markdown/with-html"; import htmlParser from 'react-markdown/plugins/html-parser'; import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' @@ -37,7 +37,7 @@ const parseHtml = htmlParser({ const renderers = { code: ({language, value}) => { - return + return } } @@ -88,8 +88,10 @@ function Question({questions}) { export default function Questions() { const classes = useStyles(); const query = useQuery(); - const {loading, error, data} = useGet(`/api/public/assignment/questions/get/${query.get('assignmentId')}`) + const assignmentId = query.get('assignmentId'); + const {loading, error, data} = useGet(`/api/public/assignment/questions/get/${assignmentId}`) + if (assignmentId === null) return if (loading) return ; if (error) return @@ -102,9 +104,20 @@ export default function Questions() { const questions = data.questions.map(translateQuestion); + if (questions.length === 0) { + return + } + return ( - + + + + Questions + + + + ) } \ No newline at end of file diff --git a/web/src/Pages/Repos/Table.jsx b/web/src/Pages/Repos/Table.jsx new file mode 100644 index 000000000..5747181c1 --- /dev/null +++ b/web/src/Pages/Repos/Table.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {makeStyles} from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import TablePagination from "@material-ui/core/TablePagination"; +import TablePaginationActions from "@material-ui/core/TablePagination/TablePaginationActions"; +import TableFooter from "@material-ui/core/TableFooter"; +import IconButton from "@material-ui/core/IconButton"; +import ExitToAppOutlinedIcon from '@material-ui/icons/ExitToAppOutlined'; +import GitHubIcon from "@material-ui/icons/GitHub"; + + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, +}); + +export default function ReposTable({rows}) { + const classes = useStyles(); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + + + + + Class + Assignment + Github Username + Repo URL + + + + {rows.map((row) => ( + + + {row.class_name} + + {row.assignment_name} + {row.github_username} + + + + + + + ))} + {emptyRows > 0 && ( + + + + )} + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/web/src/Pages/Repos/View.jsx b/web/src/Pages/Repos/View.jsx new file mode 100644 index 000000000..dd4ec76e3 --- /dev/null +++ b/web/src/Pages/Repos/View.jsx @@ -0,0 +1,46 @@ +import React from "react"; +import {Redirect} from "react-router-dom"; + +import {makeStyles} from "@material-ui/core/styles"; +import Grid from "@material-ui/core/Grid"; +import CircularProgress from "@material-ui/core/CircularProgress"; + +import ReposTable from "./Table"; +import useGet from "../../useGet"; + +const useStyles = makeStyles({ + root: { + flexGrow: 1, + }, + table: { + minWidth: 500, + }, + headerText: { + fontWeight: 600 + }, + commitHashContainer: { + width: 200, + overflow: "hidden", + } +}); + +export default function Repos() { + const classes = useStyles(); + const {loading, error, data} = useGet('/api/public/repos'); + + if (loading) return ; + if (error) return ; + + return ( + + + + + + ); +} \ No newline at end of file diff --git a/web/src/Pages/Submissions/Table.jsx b/web/src/Pages/Submissions/Table.jsx new file mode 100644 index 000000000..3a06b8220 --- /dev/null +++ b/web/src/Pages/Submissions/Table.jsx @@ -0,0 +1,141 @@ +import React from "react"; +import TableContainer from "@material-ui/core/TableContainer"; +import Paper from "@material-ui/core/Paper"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableCell from "@material-ui/core/TableCell"; +import TableBody from "@material-ui/core/TableBody"; +import {Link} from "react-router-dom"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import green from "@material-ui/core/colors/green"; +import CancelIcon from "@material-ui/icons/Cancel"; +import red from "@material-ui/core/colors/red"; +import TableFooter from "@material-ui/core/TableFooter"; +import TablePagination from "@material-ui/core/TablePagination"; +import TablePaginationActions from "@material-ui/core/TablePagination/TablePaginationActions"; +import {makeStyles} from "@material-ui/core/styles"; + +const useStyles = makeStyles({ + root: { + flexGrow: 1, + }, + table: { + minWidth: 500, + }, + headerText: { + fontWeight: 600 + }, + commitHashContainer: { + + width: 200, + overflow: "hidden", + } +}); + +export function SubmissionsTable({rows}) { + const classes = useStyles() + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + + + + + + Assignment Name + + + Commit Hash + + + Processed + + + On Time + + + Date + + + Time + + + + + + {(rowsPerPage > 0 + ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + : rows + ).map((row, ind) => ( + + + {row.assignmentName} + + + {row.commitHash.substring(0, 10)} + + + {row.processed ? : + } + + + {row.timeStamp <= row.assignmentDue ? : + } + + + {row.timeSubmitted} + + + {row.dateSubmitted} + + + ))} + + {emptyRows > 0 && ( + + + + )} + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/web/src/Pages/Submissions/View.jsx b/web/src/Pages/Submissions/View.jsx index 056931054..443bc989d 100644 --- a/web/src/Pages/Submissions/View.jsx +++ b/web/src/Pages/Submissions/View.jsx @@ -1,25 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import {makeStyles, useTheme} from '@material-ui/core/styles'; -import Table from '@material-ui/core/Table'; -import TableHead from '@material-ui/core/TableHead'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableFooter from '@material-ui/core/TableFooter'; -import TablePagination from '@material-ui/core/TablePagination'; -import TableRow from '@material-ui/core/TableRow'; -import Paper from '@material-ui/core/Paper'; -import IconButton from '@material-ui/core/IconButton'; -import FirstPageIcon from '@material-ui/icons/FirstPage'; -import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; -import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; -import LastPageIcon from '@material-ui/icons/LastPage'; -import red from '@material-ui/core/colors/red'; -import green from '@material-ui/core/colors/green'; -import CheckCircleIcon from '@material-ui/icons/CheckCircle'; -import CancelIcon from '@material-ui/icons/Cancel'; -import {Link, Redirect} from 'react-router-dom' +import {makeStyles} from '@material-ui/core/styles'; +import {Redirect} from 'react-router-dom' import CircularProgress from "@material-ui/core/CircularProgress"; import Grid from "@material-ui/core/Grid"; import Zoom from "@material-ui/core/Zoom"; @@ -27,15 +8,9 @@ import Typography from "@material-ui/core/Typography"; import useGet from '../../useGet' import {useQuery} from "../../utils"; import Questions from "../Questions/View"; +import {SubmissionsTable} from "./Table"; -const useStyles1 = makeStyles((theme) => ({ - root: { - flexShrink: 0, - marginLeft: theme.spacing(2.5), - }, -})); - -const useStyles2 = makeStyles({ +const useStyles = makeStyles({ root: { flexGrow: 1, }, @@ -52,174 +27,9 @@ const useStyles2 = makeStyles({ } }); -function TablePaginationActions(props) { - const classes = useStyles1(); - const theme = useTheme(); - const {count, page, rowsPerPage, onChangePage} = props; - - const handleFirstPageButtonClick = (event) => { - onChangePage(event, 0); - }; - - const handleBackButtonClick = (event) => { - onChangePage(event, page - 1); - }; - - const handleNextButtonClick = (event) => { - onChangePage(event, page + 1); - }; - - const handleLastPageButtonClick = (event) => { - onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( -
- - {theme.direction === 'rtl' ? : } - - - {theme.direction === 'rtl' ? : } - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="next page" - > - {theme.direction === 'rtl' ? : } - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="last page" - > - {theme.direction === 'rtl' ? : } - -
- ); -} - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - onChangePage: PropTypes.func.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, -}; - -function SubmissionsTable({rows}) { - const classes = useStyles2() - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(10); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); - - return ( - - - - - - Assignment Name - - - Commit Hash - - - Processed - - - On Time - - - Date - - - Time - - - - - - {(rowsPerPage > 0 - ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - : rows - ).map((row, ind) => ( - - - {row.assignmentName} - - - {row.commitHash.substring(0, 10)} - - - {row.processed ? : - } - - - {row.timeStamp <= row.assignmentDue ? : - } - - - {row.timeSubmitted} - - - {row.dateSubmitted} - - - ))} - - {emptyRows > 0 && ( - - - - )} - - - - - - -
-
- ) -} - export default function SubmissionsView() { - const classes = useStyles2(); + const classes = useStyles(); const query = useQuery(); const {loading, error, data} = useGet( query.get('assignmentId') @@ -234,7 +44,8 @@ export default function SubmissionsView() { return { assignmentName: assignment_name, assignmentDue: new Date(assignment_due), state: state, commitHash: commit, processed: processed, timeSubmitted: created.split(' ')[0], - dateSubmitted: created.split(' ')[1], timeStamp: new Date(created)}; + dateSubmitted: created.split(' ')[1], timeStamp: new Date(created) + }; } const rows = data.submissions @@ -243,9 +54,9 @@ export default function SubmissionsView() { return (
- @@ -253,15 +64,10 @@ export default function SubmissionsView() { CS-UY 3224 - - Questions - - - - - - + + + diff --git a/web/src/useSubscribe.jsx b/web/src/useSubscribe.jsx index 52e97ae4b..dcea9ff5e 100644 --- a/web/src/useSubscribe.jsx +++ b/web/src/useSubscribe.jsx @@ -26,9 +26,14 @@ export default function useSubscribe(path, interval, until, callback) { data: data.data.data, } setState(newState); - callback(state, newState); + if (callback) + callback(state, newState); }) .catch(function (error) { + if (!error.response) { + console.error(error) + return; + } if (error.response.status === 401) window.location = '/api/public/login'; setState({