diff --git a/.gitignore b/.gitignore index 7b2ee0e25..e0d723630 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ venv static/snapshots/ screenshots/screenshots/ +stack +grunt/node_modules +dist +dest +node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e5a9d7e07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +FROM phusion/baseimage:0.9.9 + +ENV DEBIAN_FRONTEND noninteractive +ENV HOME /root + +# Needed for this fix: http://stackoverflow.com/a/21715730 +RUN apt-get update +RUN apt-get install -y software-properties-common python-software-properties +RUN add-apt-repository ppa:chris-lea/node.js + +# Install the dependencies. +RUN apt-get update + +# New ubuntu packages should be added as their own apt-get install lines below the existing install commands +RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 libssl1.0.0 + +# PhantomJS +RUN apt-get install -y libfreetype6 libfreetype6-dev fontconfig +ADD https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-linux-x86_64.tar.bz2 phantomjs.tar.bz2 +RUN tar xjf phantomjs.tar.bz2 && ln -s `pwd`/phantomjs*/bin/phantomjs /usr/bin/phantomjs + +# Grunt +RUN apt-get install -y nodejs +RUN npm install -g grunt-cli + +ADD binary_dependencies binary_dependencies +RUN gdebi --n binary_dependencies/*.deb + +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +ADD requirements.txt requirements.txt +RUN virtualenv --distribute venv +RUN venv/bin/pip install -r requirements.txt + +ADD auth auth +ADD buildstatus buildstatus +ADD conf conf +ADD data data +ADD endpoints endpoints +ADD features features +ADD grunt grunt +ADD screenshots screenshots +ADD static static +ADD storage storage +ADD templates templates +ADD util util +ADD workers workers + +ADD app.py app.py +ADD application.py application.py +ADD config.py config.py +ADD initdb.py initdb.py + +ADD conf/init/mklogsdir.sh /etc/my_init.d/ +ADD conf/init/gunicorn.sh /etc/service/gunicorn/run +ADD conf/init/nginx.sh /etc/service/nginx/run +ADD conf/init/diffsworker.sh /etc/service/diffsworker/run +ADD conf/init/webhookworker.sh /etc/service/webhookworker/run + +RUN cd grunt && npm install +RUN cd grunt && grunt + +# Add the tests last because they're prone to accidental changes, then run them +ADD test test +RUN TEST=true venv/bin/python -m unittest discover + +RUN rm -rf /conf/stack +VOLUME ["/conf/stack", "/mnt/logs"] + +EXPOSE 443 80 + +CMD ["/sbin/my_init"] \ No newline at end of file diff --git a/README.md b/README.md index c2d045c08..f80bea5e7 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,55 @@ -to prepare a new host: - -``` -sudo apt-get update -sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core -``` - -check out the code: - -``` -git clone https://bitbucket.org/yackob03/quay.git -cd quay -virtualenv --distribute venv -source venv/bin/activate -pip install -r requirements.txt -sudo gdebi --n binary_dependencies/*.deb -sudo cp conf/logrotate/* /etc/logrotate.d/ -``` - -running: - -``` -sudo mkdir -p /mnt/logs/ && sudo chown $USER /mnt/logs/ && sudo /usr/local/nginx/sbin/nginx -c `pwd`/conf/nginx.conf -sudo mkdir -p /mnt/logs/ && sudo chown $USER /mnt/logs/ && STACK=prod gunicorn -c conf/gunicorn_config.py application:application -``` - -start the log shipper: +to build and upload quay to quay: ``` curl -s https://get.docker.io/ubuntu/ | sudo sh +sudo apt-get update && sudo apt-get install -y git +git clone git clone https://bitbucket.org/yackob03/quay.git +cd quay +sudo docker build -t quay.io/quay/quay . +sudo docker push quay.io/quay/quay +``` + +to prepare a new host: + +``` +curl -s https://get.docker.io/ubuntu/ | sudo sh +sudo apt-get update && sudo apt-get install -y git +git clone https://github.com/DevTable/gantryd.git +cd gantryd +cat requirements.system | xargs sudo apt-get install -y +virtualenv --distribute venv +venv/bin/pip install -r requirements.txt +sudo docker login -p 9Y1PX7D3IE4KPSGCIALH17EM5V3ZTMP8CNNHJNXAQ2NJGAS48BDH8J1PUOZ869ML -u 'quay+deploy' -e notused quay.io +``` + +start the quay processes: + +``` +cd ~ +git clone https://bitbucket.org/yackob03/quayconfig.git +sudo docker pull quay.io/quay/quay +sudo mkdir -p /mnt/logs/ +cd ~/gantryd +sudo venv/bin/python gantry.py ../quayconfig/production/gantry.json update quay +``` + +start the log shipper (DEPRECATED): + +``` sudo docker pull quay.io/quay/logstash sudo docker run -d -e REDIS_PORT_6379_TCP_ADDR=logs.quay.io -v /mnt/logs:/mnt/logs quay.io/quay/logstash quay.conf ``` -start the workers: - -``` -STACK=prod python -m workers.diffsworker -D -STACK=prod python -m workers.webhookworker -D -``` - -bouncing the servers: - -``` -sudo kill -HUP `cat /mnt/logs/nginx.pid` -kill -HUP `cat /mnt/logs/gunicorn.pid` - -kill -restart daemons -``` - running the tests: ``` -STACK=test python -m unittest discover +TEST=true python -m unittest discover ``` running the tests with coverage (requires coverage module): ``` -STACK=test coverage run -m unittest discover +TEST=true coverage run -m unittest discover coverage html ``` diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 000000000..5089cce9c --- /dev/null +++ b/alembic.ini @@ -0,0 +1,58 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = data/migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +sqlalchemy.url = sqlite:///will/be/overridden + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app.py b/app.py index 488c108d4..aa663418c 100644 --- a/app.py +++ b/app.py @@ -1,48 +1,48 @@ import logging import os -import stripe from flask import Flask from flask.ext.principal import Principal from flask.ext.login import LoginManager from flask.ext.mail import Mail -from config import (ProductionConfig, DebugConfig, LocalHostedConfig, - TestConfig, StagingConfig) -from util import analytics +import features + +from storage import Storage +from data.userfiles import Userfiles +from util.analytics import Analytics +from util.exceptionlog import Sentry +from data.billing import Billing + + +OVERRIDE_CONFIG_FILENAME = 'conf/stack/config.py' app = Flask(__name__) logger = logging.getLogger(__name__) -stack = os.environ.get('STACK', '').strip().lower() -if stack.startswith('prod'): - logger.info('Running with production config.') - config = ProductionConfig() -elif stack.startswith('staging'): - logger.info('Running with staging config on production data.') - config = StagingConfig() -elif stack.startswith('localhosted'): - logger.info('Running with debug config on production data.') - config = LocalHostedConfig() -elif stack.startswith('test'): - logger.info('Running with test config on ephemeral data.') - config = TestConfig() +if 'TEST' in os.environ: + from test.testconfig import TestConfig + logger.debug('Loading test config.') + app.config.from_object(TestConfig()) else: - logger.info('Running with debug config.') - config = DebugConfig() + from config import DefaultConfig + logger.debug('Loading default config.') + app.config.from_object(DefaultConfig()) -app.config.from_object(config) + if os.path.exists(OVERRIDE_CONFIG_FILENAME): + logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME) + app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME) + +features.import_features(app.config) Principal(app, use_sessions=False) -login_manager = LoginManager() -login_manager.init_app(app) - -mail = Mail() -mail.init_app(app) - -stripe.api_key = app.config.get('STRIPE_SECRET_KEY', None) - -mixpanel = app.config['ANALYTICS'].init_app(app) +login_manager = LoginManager(app) +mail = Mail(app) +storage = Storage(app) +userfiles = Userfiles(app) +analytics = Analytics(app) +billing = Billing(app) +sentry = Sentry(app) diff --git a/application.py b/application.py index 91062d4f6..e7cb11c76 100644 --- a/application.py +++ b/application.py @@ -1,10 +1,12 @@ import logging +import logging.config +import uuid from app import app as application -from data.model import db as model_db +from flask import request, Request +from util.names import urn_generator -# Initialize logging -application.config['LOGGING_CONFIG']() +from data.model import db as model_db # Turn off debug logging for boto logging.getLogger('boto').setLevel(logging.CRITICAL) @@ -20,6 +22,7 @@ from endpoints.callbacks import callback logger = logging.getLogger(__name__) +profile = logging.getLogger('application.profiler') application.register_blueprint(web) application.register_blueprint(callback, url_prefix='/oauth2') @@ -30,6 +33,29 @@ application.register_blueprint(api_bp, url_prefix='/api') application.register_blueprint(webhooks, url_prefix='/webhooks') application.register_blueprint(realtime, url_prefix='/realtime') +class RequestWithId(Request): + request_gen = staticmethod(urn_generator(['request'])) + + def __init__(self, *args, **kwargs): + super(RequestWithId, self).__init__(*args, **kwargs) + self.request_id = self.request_gen() + +@application.before_request +def _request_start(): + profile.debug('Starting request: %s', request.path) + + +@application.after_request +def _request_end(r): + profile.debug('Ending request: %s', request.path) + return r + +class InjectingFilter(logging.Filter): + def filter(self, record): + record.msg = '[%s] %s' % (request.request_id, record.msg) + return True + +profile.addFilter(InjectingFilter()) def close_db(exc): db = model_db @@ -38,6 +64,8 @@ def close_db(exc): db.close() application.teardown_request(close_db) +application.request_class = RequestWithId if __name__ == '__main__': + logging.config.fileConfig('conf/logging_local.conf', disable_existing_loggers=False) application.run(port=5000, debug=True, threaded=True, host='0.0.0.0') diff --git a/auth/permissions.py b/auth/permissions.py index 59af7be42..b2e7fe784 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -22,6 +22,7 @@ _TeamTypeNeed = namedtuple('teamwideneed', ['type', 'orgname', 'teamname', 'role _TeamNeed = partial(_TeamTypeNeed, 'orgteam') _UserTypeNeed = namedtuple('userspecificneed', ['type', 'username', 'role']) _UserNeed = partial(_UserTypeNeed, 'user') +_SuperUserNeed = partial(namedtuple('superuserneed', ['type']), 'superuser') REPO_ROLES = [None, 'read', 'write', 'admin'] @@ -88,6 +89,11 @@ class QuayDeferredPermissionUser(Identity): logger.debug('Loading user permissions after deferring.') user_object = model.get_user(self.id) + # Add the superuser need, if applicable. + if (user_object.username is not None and + user_object.username in app.config.get('SUPER_USERS', [])): + self.provides.add(_SuperUserNeed()) + # Add the user specific permissions, only for non-oauth permission user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin')) logger.debug('User permission: {0}'.format(user_grant)) @@ -171,6 +177,11 @@ class CreateRepositoryPermission(Permission): super(CreateRepositoryPermission, self).__init__(admin_org, create_repo_org) +class SuperUserPermission(Permission): + def __init__(self): + need = _SuperUserNeed() + super(SuperUserPermission, self).__init__(need) + class UserAdminPermission(Permission): def __init__(self, username): diff --git a/binary_dependencies/builder/linux-headers-3.11.0-17-generic_3.11.0-17.28_amd64.deb b/binary_dependencies/builder/linux-headers-3.11.0-17-generic_3.11.0-17.28_amd64.deb deleted file mode 100644 index b69f98a44..000000000 Binary files a/binary_dependencies/builder/linux-headers-3.11.0-17-generic_3.11.0-17.28_amd64.deb and /dev/null differ diff --git a/binary_dependencies/builder/linux-headers-3.11.0-17_3.11.0-17.28_all.deb b/binary_dependencies/builder/linux-headers-3.11.0-17_3.11.0-17.28_all.deb deleted file mode 100644 index c9e530479..000000000 Binary files a/binary_dependencies/builder/linux-headers-3.11.0-17_3.11.0-17.28_all.deb and /dev/null differ diff --git a/binary_dependencies/builder/linux-image-3.11.0-17-generic_3.11.0-17.28_amd64.deb b/binary_dependencies/builder/linux-image-3.11.0-17-generic_3.11.0-17.28_amd64.deb deleted file mode 100644 index cf0dce064..000000000 Binary files a/binary_dependencies/builder/linux-image-3.11.0-17-generic_3.11.0-17.28_amd64.deb and /dev/null differ diff --git a/binary_dependencies/builder/linux-image-extra-3.11.0-17-generic_3.11.0-17.28_amd64.deb b/binary_dependencies/builder/linux-image-extra-3.11.0-17-generic_3.11.0-17.28_amd64.deb deleted file mode 100644 index bdee3f6af..000000000 Binary files a/binary_dependencies/builder/linux-image-extra-3.11.0-17-generic_3.11.0-17.28_amd64.deb and /dev/null differ diff --git a/binary_dependencies/builder/lxc-docker-0.9.0-tutum2_0.9.0-tutum2-20140327210604-4c49268-dirty_amd64.deb b/binary_dependencies/builder/lxc-docker-0.9.0-tutum2_0.9.0-tutum2-20140327210604-4c49268-dirty_amd64.deb deleted file mode 100644 index 318bf9758..000000000 Binary files a/binary_dependencies/builder/lxc-docker-0.9.0-tutum2_0.9.0-tutum2-20140327210604-4c49268-dirty_amd64.deb and /dev/null differ diff --git a/binary_dependencies/builder/lxc-docker-0.9.0_0.9.0-20140501212101-72572f0-dirty_amd64.deb b/binary_dependencies/builder/lxc-docker-0.9.0_0.9.0-20140501212101-72572f0-dirty_amd64.deb new file mode 100644 index 000000000..2242c23ce Binary files /dev/null and b/binary_dependencies/builder/lxc-docker-0.9.0_0.9.0-20140501212101-72572f0-dirty_amd64.deb differ diff --git a/binary_dependencies/nginx_1.4.2-nobuffer-1_amd64.deb b/binary_dependencies/nginx_1.4.2-nobuffer-1_amd64.deb deleted file mode 100644 index 7f3119b61..000000000 Binary files a/binary_dependencies/nginx_1.4.2-nobuffer-1_amd64.deb and /dev/null differ diff --git a/binary_dependencies/nginx_1.4.2-nobuffer-2_amd64.deb b/binary_dependencies/nginx_1.4.2-nobuffer-2_amd64.deb new file mode 100644 index 000000000..dfc530e7f Binary files /dev/null and b/binary_dependencies/nginx_1.4.2-nobuffer-2_amd64.deb differ diff --git a/conf/certs/digital_ocean b/conf/certs/digital_ocean deleted file mode 100644 index 700f2b586..000000000 --- a/conf/certs/digital_ocean +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAwjlIK0HodmDNrZAmaALtr9RLriRSeeLh76gV8KHmjRweeT7v -dmhKeGP1nOAs17caZkcwsW0tiDbCeIv2MisV405sScjPOxFivWpY8tL72sgVuOAl -ReZauOGZ4M1ZcSa/YbT7tnFCIayYE9pde4ih5LmYZqKsBsaNq3ErcMnAzqG77D95 -8swuVwhz/INioBWwe4FjO76/0DqS357hT5yHDWthJD6UUH12VajPKBtXEvGNUtNL -vdq+drm9omt2y0seMn47fZXiNIulLv7ojsWKwtRMTsGcjnv6VMZAVAuX11u4cJd+ -oPTbDl0D+02B7XYcxABqdMZcOc1/7VTUlFFd4wIDAQABAoIBAAs4V+z3z8AW84rV -SwKzOJvxvbV/r6wO6VJ4+Vt/XtxEBZanhhnnCHZP//5iDPUhRMsnza5SSlEWKMHi -BAT97DPHcgYJLb+Rz4x1ulG80oPfDzIw8LZLCm6nycXs1v/sZx3z4J63iER9vgNX -mBLs371g42b6esmhasm+re3EGflV0LeY1IX0MY40pqGndmW8Fly1QH179TrMzVUJ -btu3i2JrwWmKk5zO5YGm0SYY5QQGCdjPj6SL+idDniAefEvbjJYz2qOaPOF3wj/7 -r8dAnmyaP10Q3JojT01Et5ltMfr0oF2/pic9tWYGrgn/aIuoXUXj0SF3Pfgrb/4L -Et1kzFECgYEA8Tb/9bYzQgtaQTQfzFU/KnsIKKnrxh73rZwnIxG59WvN0Ws41Byf -rv8fEbXWU8Yj0drxRSud9fADr99lZGWFxle8rSW5+qqoUxG8n/fkktzHxyPE/9Mh -pZW7un7a5/glKgUpHpjaOCZj9rhdF1AwdUXLSo1sFc7VBsKvKiKJAT0CgYEAziDt -A9h5lOgiLGf1xdBq3qmLIlARz7fivAcZ5acSGN5k6MFFxjHNqhcXRusqs7g+hvCN -eRupdwfgSdLwrTfvxuY4pCcddfYIZO3uUZYs/glvYRtIxaP2kMBkZTs9KzI02Bjv -zT3NPReR/46SqW0zvYTlRFSY7VZ0eRED/5xnjZ8CgYAZdlrSjyceA6DFXUE2CpGe -ZFpaIIW45i/y7ZbcBtUAaR7SymS3T0Yz7M5UykMTmMjTMC9jw9Tqzyk0eXp0fJsA -cuaByIe3RCh8jFTC9iH0tsWH6eizsI/OsN2eNCHbdsBFjUHn7u6qGrNWqeN5wIc8 -+d8ZwY/1RV4LVqWy5u5baQKBgHLFvJMWluQFuPl2zU9etBLU3ma1pKU/I11EqvPH -afk044UCEKLBml1pzAkt6jH1lcM2798OOvbPCOCyNlaMvdLG36TvLqU+3/+qx7bf -4p90i3LLaWK64BBLP9tp9640n13vzJ5AGiY5GI7uSNVTu6p789hvLlOAfwvmII7T -/IjLAoGBAO6iU8i6pAOaKa7+/uExXx6xwk3vqQtovxByo1/m7NpyUtT+ElDSq+t9 -7f+3TzzPB6ggdMl8d+PSyHR3o7KjVPgOSe7zld7eePhUrLjwZ4lh5ohcvhvYfaRL -0EgRTaTb+zLtCAvJS/ilNnJoIcxUmD8u5uSXpY7vAleSOiQTJRTh ------END RSA PRIVATE KEY----- diff --git a/conf/certs/digital_ocean.pub b/conf/certs/digital_ocean.pub deleted file mode 100644 index 95db83601..000000000 --- a/conf/certs/digital_ocean.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCOUgrQeh2YM2tkCZoAu2v1EuuJFJ54uHvqBXwoeaNHB55Pu92aEp4Y/Wc4CzXtxpmRzCxbS2INsJ4i/YyKxXjTmxJyM87EWK9aljy0vvayBW44CVF5lq44ZngzVlxJr9htPu2cUIhrJgT2l17iKHkuZhmoqwGxo2rcStwycDOobvsP3nyzC5XCHP8g2KgFbB7gWM7vr/QOpLfnuFPnIcNa2EkPpRQfXZVqM8oG1cS8Y1S00u92r52ub2ia3bLSx4yfjt9leI0i6Uu/uiOxYrC1ExOwZyOe/pUxkBUC5fXW7hwl36g9NsOXQP7TYHtdhzEAGp0xlw5zX/tVNSUUV3j jake@coreserver diff --git a/conf/certs/quay-enc.key b/conf/certs/quay-enc.key deleted file mode 100644 index 662bbaf52..000000000 --- a/conf/certs/quay-enc.key +++ /dev/null @@ -1,54 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,EE6906CB831EE3514F826A95533805EF - -BQFBgkKWIRGnE9h53VAsvWptKyEtenBWNV1tdiR3AMTJMrpi/f5z6KbDpDeO86st -N8GhmlfssfjK0eQWYSxTp4ZPoF2C2EjKE21buZ6ML6xeAJQSmca+owWkgCFdkndT -MhdhDeEyxAA2rd41QYMf99iFrzQqfqZTBfkQh27x8+Q5T/4cE8BeDB7S+Y+WKDGm -cVGudSuhiVTZcgIwErABkSJmMNzKhHZGOsUnYvHUE0T3z+LH6R9dtpeZpyZ3rrO2 -VH0qy3o7Oy20M4RXqrK7jp60wX7ZB3/VLmy8+6Jp7VUqZgyATIoBtKFP9QKCJCj4 -bvwFou7svhMwfg76+lqCcQm+qMgdIsBUXpPPTxcMEEGxHfVhypZMVzYB6H9GRkcY -dtpx70V22TG/ER7aQ5pthxx9h4CiIc7E5CEPJXdkVMWpLXhtAdpT33lP3cdOdbph -8+9W74UQGMjz7nOl0mYTP2z/4sGRxDVqOAgOdcvxm2TRwtemf68Pd50B8WpKlM9Q -LukFzOWYgUjogCoCxWOKfh6o1CcC6rarGVHDxb8qZ8fYwtkNynoADNCvj9EQjw9j -D85CgSXwPNTD6EdsdaSolYTJy5/C229Vo6YCbmKg/SZy+KtrLk2AzPmcInBz/aHz -JWun7lGks2JoLQZsKFrc3P9GDiLsvOCojrJsLR8H69jXLe4d8DwD8n1Hq18QIfCX -qBGJygkvZdNMj4fgU2GWbA2bus1ihIGIZYrv11MdHgoTvsCkzW0MUJ8/yJPmEoMv -vTN3b+iDiBD2rVlCd/rQ0aQpU7XDjzgPEA19UcT13VwKodQKGroS51Qwhb7ujkMq -Bz4vk3ZoGVqtQeMVNvMEUL2yaj/7wbrwiHTFsts56jzBlEMoNjMz5rb9OAowMvvM -Gz5BlpecXw3VGRV39LR5Oub3MhJW8O4kKuj3LDTurSF2sWh6OvJXnvrzJA+DSfAf -IQmocKqFX7zpmKHrTMf11pcKA+EGZ/0tAzP6sfL2RlUYVzTRc4Af1ZoorYuVFWcX -pGz7KtCQ1lH7ErSVnjVI6rWkhN3FKXVxVV9SVcWsWCCLMORCQ069EQyECb9HOhMB -RB54e926p4JBTE38R8t+F1g38Smfj6+/HW9PvEzmJVXTZSC5XibI+n/cVHf4RVBh -tZ9LlPfA53YtTVedXoo0buFHOJYae8FvX+un/dXSQjQcVvnNuNIE6Ufqlw6k7miv -ZU29ZOjfCSpJhWKPTqZ6xBhmYHKHssSV6BBcsH9POs3UQ0vYtO3CyoL5QWf8SYtD -1Fy+6tyPyhjN3nXzyezHhz4ziW0bU6O3fF2/wcmqT5cslw5+zD8jXr/QKOHEqFEm -Ekk8lHcSvwcrgGFp896UfK1luBz2ZrfhV79IxCouPv2eXQiudPfXxN8ESswZE5S+ -bzwTgpaCUuZ1CxNfj+5SN19akfcgBQD1Wfvnwu/Ii4kZ3LA0LwzArIiNOlq96FkR -Tky2vScpwtgQDcNiQGCL3JghLW6bG3MiV/Yz5Aa9S8oSaesrcdb8iYp/rEqULHi8 -D9hEdxT0Shs+PVI3yvR6G48B0wPF06/aH6lpk2yFkXsKcvPeRgME2qfGmF8J9xGL -HNiishjhonT5mlCmcXjMBVsJ9G06IsBt6PCjrzMft3mo43ZVGpS2+bjzzNsw2xVH -VKAXj74qFzer32i80FSBRrBZ7UhJ/By+lJEf2JTvzbW1HwKxEO3Qis4/qm7m6ob1 -0lUMtCEniOZUbhc2iLkl8d2D+TGSjVxC0C2avOrNqyj9dwKqA597kiLQAau70PL2 -uCCbBVIg1YrtGmxKiCbbzQyXAUwjNM4Wi0/ynS8+kOvpj2LOUuZYkf/xPnhhBE6y -kcT2Dd/S+PMBA1dFPNDMX/DTNZM5Isjld3uh6vTLWf2un7ZBwW0k/RZC5z6H803O -x9/kPCqGhWyYFR8JNF4/D/+84Fvnpxdm6PKpMdJcv27EuBAZ3t8ckg/PQ7qMK+q7 -6gSwNoqZ6Pu1F278igYhl15t1B1EWKq+wVLRns2jpoKLC50xBFCwRlcfvUg0AqF+ -jK8ohHxMUDQLm+nnJfI//uleNnYtJ3iHvaoYVtUhO3Wm+5nulWy5tXn9MPao8jne -D6YYi2En1xqQsB4JUd0rn5/pZ4SIJPEUJXPVoTy/EEv4T/yUxWnt+817kgYLafnl -jlgisb3hananDYNRKXOf708Rv5nH9bJgZbaQm1p8ktF+LzWF6eTmEX3JmXJ2VWqM -aJPx9TUn2gAns0hgk5xmClSFmsk8ft5oZjdm9kdKQLzGEgQt/YXtHmeZoWF3pCOz -uaYoWLHa91PoXQqxp1WdkDiF0WhQBYGb4R1ZRz5tAOfg0fY3CRQ9RksPvXal0fD5 -RVWf/qCAM9HlpsJFwVDDc1LTPimQqui76FU2adLJxxTJyemUEpz/cQPOzWmA9nOy -NQ4i1JpvXRPaLTRqaae6lw+TsuXidCOoU4y/5zM7o4DgsFXmAyTDyXlzSdafKOjd -79GtcdlSKY3nS/v6N2Df51hzjBbpVXBKIleZD7iGQSpGVoTOC7c1RVLXWYcxCz0F -m1kj8yIXIyb07dbb5FzdNGfoVj8xDNfhxclK1b32EqIWGR/3vBx3+KZ3uX4N5/Vg -/sDuRMpGc4fj3E+BYRmsm4duFVNB+YV6LDG98ZUvXm5od9gmmCzK/efU+yUVVaUP -lNn+rG4lbiRtovjua7GvL5I8br8Dr2qBF8FRwejrh1wcnlFYvQVfOEhZM4h6IoEn -bPieKMqDOKsmumcg429m/dV5PEwd4j9yy998DjXvPU2GJ41UHiZdrJis3rPYsqW/ -PaIhOrAkdSQmZghAswzJz5wxeXJllusCnSvDbLmNhAyKGZW8UhygeLBUndK6v0Bl -WyEimJ9Jebuggdq5tehKE6UOtRoZ1QP5OV9dAmxxvtaOX8iIbkuno3x7SvUHfD2P -whh74xB7y+WQVqqiZYPgFVtvPwOW728j6TUWBI9wKY2l0683LRkq0cfooRyjVula -ydpIVsMUmCZeAdOKeXaqA23kZNvctt7eubSVr9TtzB8yjEtmyn+eehQEbvx1NIgE -QcvBB55x3/SS6cJhCLg+ypSVueEzA8vWkwzp1EgAt2QSvuL7e+J40vHEwNKCfmxl ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/conf/certs/quay-staging-enc.key b/conf/certs/quay-staging-enc.key deleted file mode 100644 index 2e4e0ec73..000000000 --- a/conf/certs/quay-staging-enc.key +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,2E92F159A4A60E842528201723AF241C - -fThdbyOk4whle4mTLn3fDt/eV/JqKXxs18yQl6OdS26sxB80MmwIAF6dyZI3AXHC -CBS6mmdZtFgqodXs125XXPijJ0m0V5s2yx7EHeqPMEYLnzu21QfsbtRGXtl5EX3m -il+F9Y/+Y3B0ZtvMcRsGPjnCkLeQZwMOVc8DsgC92bqgu3sCDuz70W6Na6wvIlWU -2/OViNDSI6QUo90bkdf1H/+pCr8Dge9MBBMnU2G7DqsaLR+ehUMj7sZs/MJjUOK5 -zERfQMEoywqBatQUlP203GWe3hsMAqAKnc5VUc52mpT7uC8afLuSqqu2SpP9W0f2 -Fu6jv0D9JqFI7tf11sGYyWdMoP7zSxcquRC+NMVfvEvlRfjourZ8LAkdkQioeGQO -uLUoXe55kDfcJMEahUNMbmrwA7pqfsfGkKCMnBTCATvjtghTTCL4xVrK7jwXk7Sv -q54TQe27sryYCGnCLErGxEwjs0lnIWsZ+ePa4qgF05JBty3psrFHZlubG+CT8sb7 -uzBwzjsIW4rgaBdxKzBgcYoUDhxGMqsg3DxcXh9EomsK/ka+SlVnSihYpxnuerxG -LYgSQhfSRZRTtHUzQcQuvT5sfa1UQIWWWxeUKxuTTcyE3g1DY3/osatchW9+CCW2 -z10daPvfdq70OiEnPLSDvD1Gs49QQlfsoaq4lhNU7VBqznr/5bdC4iiQssXbUOjM -odREtw0d8Ox72/V/z01/QjTiOIg5tk9tGORVFPmLHC/Db3OUkLHsp/ls8E2IkIMU -wrbmvA9ABLgfpLlgivTtYjkG+H9BtSbi4jGHjYGR40unHqzY+6EQfoL7hu6zzacB -4jOhKKtR1yskK42VI6vrihT3GaA3MslCBNmEUv/4FkbvRkVifDd7hMylHCkjlUSm -zpaydx2gVrRYlum/Ipd+yPb95iniVlaaNe9ZLqtpZt15mEgLwWfrTq470NGLKwbg -7IyIrN6TjHSy1BQMS7kQVzc8Mgafa5O3pXr3zJS2JCg13FC7DarRZfNlqQZ7ErRA -imF5jxMQC8agbairlrGQ2i+ckY2wm8OEKUu3f8+O1Rq6rHy7SLQq1PSoQQetstFz -jLDPRFvtymZb0e510nbsLOcQvaWjda2sU367ed99TkVm0J7PJG3J38B7Sz0Q82hh -jfoYsu+kyNCcR2eS7F3E/MAr85r5qW8/0oewV0ZNxZLyAqJLFHKaeEaGcW0q/wb3 -oDcli8kbX0YnMfm4p7YaR++VYtfiQrhzOkzYNmRV4xbVF3eE6f3lSjGXmeqWhM5c -o0+I75UvbjZzvfWjekkwKhzuIHRJV6g5ldxuaJ290xQrdbpzO0xmYKbOHkEtIjow -pBIaA6vgmOTnREdQQSjcNcD+eZDuzaKGwCa2IH5k5b025IOWS4XyZ2JNmRzgw2d8 -mF6fO8ZDpcQUMIv1Hbn4Yc7kE1vOPkUaB3pKumaQW16COoi+EtY+umWf2/SJb+Cd -PGfSzcagiR5Fs0wKgHCVTMuoKR/xhaSqLUkCgQoXKRNoT5cO/sTiYygDWyeyRnCE -CA3NKDsDnaoCAeDU5XS1tmNJSIxlQjlnFHP71otxMKtV/g42vwTkXmJ+6gH5g9TG ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/conf/certs/quay-staging-unified.cert b/conf/certs/quay-staging-unified.cert deleted file mode 100644 index b2c0b5fce..000000000 --- a/conf/certs/quay-staging-unified.cert +++ /dev/null @@ -1,116 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGTjCCBTagAwIBAgIDDK/RMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ -TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg -MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMxMDI5MTIxNjM2 -WhcNMTQxMDMwMTUyMDI0WjBlMRkwFwYDVQQNExBXOHJYcjhsNVBEMkpMQ0VRMQsw -CQYDVQQGEwJVUzEYMBYGA1UEAxMPc3RhZ2luZy5xdWF5LmlvMSEwHwYJKoZIhvcN -AQkBFhJob3N0bWFzdGVyQHF1YXkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDq45PoBSyAniiglXyt5yI3kbLcwLXRTrWNCv0rxi+57Elxs/ix7RII -Ig1iO1FARJP/ipRRHFB8GNuG+eIdJAEaeB39eyjvGvsOcE8hlK1Hu3Hd3PcKAwaV -JpVZyTblUYXy55kw9okwNZJpVJPOHxKaOjNYrJynw92VJ21WeeGk+kh0EZKQ4vtp -sMMYIJapQk1CYDdreZoA0TEGZixJG8laUfX+S+CJf9KY7qH8LefjK9fr6x7R+qd4 -Hvj6lwtGV5UEBkGtU2bzTAOSEMOJBcxOfrPovFFLVvtbYCRAIY2Y5PPvV1Wna9sB -h52hxRhoJpwU3/g+LXhJ6UEGFOMvfa+hAgMBAAGjggLdMIIC2TAJBgNVHRMEAjAA -MAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU6rqK -WYOkW9Ya0nerdfpJGA5GbxcwHwYDVR0jBBgwFoAU60I00Jiwq5/0G2sI98xkLu8O -LEUwIwYDVR0RBBwwGoIPc3RhZ2luZy5xdWF5LmlvggdxdWF5LmlvMIIBVgYDVR0g -BIIBTTCCAUkwCAYGZ4EMAQIBMIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUF -BwIBFiJodHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEF -BQcCAjCB6jAnFiBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEB -GoG+VGhpcyBjZXJ0aWZpY2F0ZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUg -Q2xhc3MgMSBWYWxpZGF0aW9uIHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20g -Q0EgcG9saWN5LCByZWxpYW5jZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9z -ZSBpbiBjb21wbGlhbmNlIG9mIHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25z -LjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQx -LWNybC5jcmwwgY4GCCsGAQUFBwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9v -Y3NwLnN0YXJ0c3NsLmNvbS9zdWIvY2xhc3MxL3NlcnZlci9jYTBCBggrBgEFBQcw -AoY2aHR0cDovL2FpYS5zdGFydHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMS5zZXJ2 -ZXIuY2EuY3J0MCMGA1UdEgQcMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzAN -BgkqhkiG9w0BAQUFAAOCAQEAfhP/++WewlphCojZwXijpFIy+XX1gR0p4kSfxVgA -Anl3khFL/xAvhk6pbWjGQM/9FWb/PFDRgj4fvMKGR8F9bMKNfBOrT+SyWDuI1Ax3 -y0unu0vZjEfUJmMktrr2aN3NI/bBmdVixNntsHRB0yrrl7Zk0TjQM3I1egfygoxa -tfARn5QOO/sReYJXlJdwmFMH0dpMT3++p5RhMZPDVeAdUUK/KzSdlPkVrLPJTKEY -d+IAIWjZq5CGOjM9052+CDhyAMvdywJQpxuhO/BzmPrt0ZQwuMdTUutPT2ijDGCB -J7nCUCVEtF25KJrJxeXY6oLxgXoaqqU1ZGivAS1oCtnocg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGNDCCBBygAwIBAgIBGDANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg -Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjA1NDE3WhcNMTcxMDI0MjA1NDE3WjCB -jDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsT -IlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0 -YXJ0Q29tIENsYXNzIDEgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtonGrO8JUngHrJJj0PREGBiE -gFYfka7hh/oyULTTRwbw5gdfcA4Q9x3AzhA2NIVaD5Ksg8asWFI/ujjo/OenJOJA -pgh2wJJuniptTT9uYSAK21ne0n1jsz5G/vohURjXzTCm7QduO3CHtPn66+6CPAVv -kvek3AowHpNz/gfK11+AnSJYUq4G2ouHI2mw5CrY6oPSvfNx23BaKA+vWjhwRRI/ -ME3NO68X5Q/LoKldSKqxYVDLNM08XMML6BDAjJvwAwNi/rJsPnIO7hxDKslIDlc5 -xDEhyBDBLIf+VJVSH1I8MRKbf+fAoKVZ1eKPPvDVqOHXcDGpxLPPr21TLwb0pwID -AQABo4IBrTCCAakwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFOtCNNCYsKuf9BtrCPfMZC7vDixFMB8GA1UdIwQYMBaAFE4L7xqkQFul -F2mHMMo0aEPQQa7yMGYGCCsGAQUFBwEBBFowWDAnBggrBgEFBQcwAYYbaHR0cDov -L29jc3Auc3RhcnRzc2wuY29tL2NhMC0GCCsGAQUFBzAChiFodHRwOi8vd3d3LnN0 -YXJ0c3NsLmNvbS9zZnNjYS5jcnQwWwYDVR0fBFQwUjAnoCWgI4YhaHR0cDovL3d3 -dy5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0 -c3NsLmNvbS9zZnNjYS5jcmwwgYAGA1UdIAR5MHcwdQYLKwYBBAGBtTcBAgEwZjAu -BggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjA0 -BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50ZXJtZWRpYXRl -LnBkZjANBgkqhkiG9w0BAQUFAAOCAgEAIQlJPqWIbuALi0jaMU2P91ZXouHTYlfp -tVbzhUV1O+VQHwSL5qBaPucAroXQ+/8gA2TLrQLhxpFy+KNN1t7ozD+hiqLjfDen -xk+PNdb01m4Ge90h2c9W/8swIkn+iQTzheWq8ecf6HWQTd35RvdCNPdFWAwRDYSw -xtpdPvkBnufh2lWVvnQce/xNFE+sflVHfXv0pQ1JHpXo9xLBzP92piVH0PN1Nb6X -t1gW66pceG/sUzCv6gRNzKkC4/C2BBL2MLERPZBOVmTX3DxDX3M570uvh+v2/miI -RHLq0gfGabDBoYvvF0nXYbFFSF87ICHpW7LM9NfpMfULFWE7epTj69m8f5SuauNi -YpaoZHy4h/OZMn6SolK+u/hlz8nyMPyLwcKmltdfieFcNID1j0cHL7SRv7Gifl9L -WtBbnySGBVFaaQNlQ0lxxeBvlDRr9hvYqbBMflPrj0jfyjO1SPo2ShpTpjMM0InN -SRXNiTE8kMBy12VLUjWKRhFEuT2OKGWmPnmeXAhEKa2wNREuIU640ucQPl2Eg7PD -wuTSxv0JS3QJ3fGz0xk+gA2iCxnwOOfFwq/iI9th4p1cbiCJSS4jarJiwUW0n6+L -p/EiO/h94pDQehn7Skzj0n1fSoMD7SfWI55rjbRZotnvbIIp3XUZPD9MEI3vu3Un -0q6Dp6jOW6c= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg -Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 -MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi -U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh -cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk -pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf -OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C -Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT -Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi -HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM -Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w -+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ -Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 -Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B -26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID -AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE -FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j -ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js -LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM -BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 -Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy -dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh -cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh -YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg -dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp -bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ -YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT -TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ -9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 -jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW -FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz -ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 -ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L -EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu -L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq -yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC -O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V -um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh -NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= ------END CERTIFICATE----- diff --git a/conf/certs/quay-staging.cert b/conf/certs/quay-staging.cert deleted file mode 100644 index f02c8de26..000000000 --- a/conf/certs/quay-staging.cert +++ /dev/null @@ -1,36 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGTjCCBTagAwIBAgIDDK/RMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ -TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg -MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMxMDI5MTIxNjM2 -WhcNMTQxMDMwMTUyMDI0WjBlMRkwFwYDVQQNExBXOHJYcjhsNVBEMkpMQ0VRMQsw -CQYDVQQGEwJVUzEYMBYGA1UEAxMPc3RhZ2luZy5xdWF5LmlvMSEwHwYJKoZIhvcN -AQkBFhJob3N0bWFzdGVyQHF1YXkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDq45PoBSyAniiglXyt5yI3kbLcwLXRTrWNCv0rxi+57Elxs/ix7RII -Ig1iO1FARJP/ipRRHFB8GNuG+eIdJAEaeB39eyjvGvsOcE8hlK1Hu3Hd3PcKAwaV -JpVZyTblUYXy55kw9okwNZJpVJPOHxKaOjNYrJynw92VJ21WeeGk+kh0EZKQ4vtp -sMMYIJapQk1CYDdreZoA0TEGZixJG8laUfX+S+CJf9KY7qH8LefjK9fr6x7R+qd4 -Hvj6lwtGV5UEBkGtU2bzTAOSEMOJBcxOfrPovFFLVvtbYCRAIY2Y5PPvV1Wna9sB -h52hxRhoJpwU3/g+LXhJ6UEGFOMvfa+hAgMBAAGjggLdMIIC2TAJBgNVHRMEAjAA -MAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU6rqK -WYOkW9Ya0nerdfpJGA5GbxcwHwYDVR0jBBgwFoAU60I00Jiwq5/0G2sI98xkLu8O -LEUwIwYDVR0RBBwwGoIPc3RhZ2luZy5xdWF5LmlvggdxdWF5LmlvMIIBVgYDVR0g -BIIBTTCCAUkwCAYGZ4EMAQIBMIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUF -BwIBFiJodHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEF -BQcCAjCB6jAnFiBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEB -GoG+VGhpcyBjZXJ0aWZpY2F0ZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUg -Q2xhc3MgMSBWYWxpZGF0aW9uIHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20g -Q0EgcG9saWN5LCByZWxpYW5jZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9z -ZSBpbiBjb21wbGlhbmNlIG9mIHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25z -LjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQx -LWNybC5jcmwwgY4GCCsGAQUFBwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9v -Y3NwLnN0YXJ0c3NsLmNvbS9zdWIvY2xhc3MxL3NlcnZlci9jYTBCBggrBgEFBQcw -AoY2aHR0cDovL2FpYS5zdGFydHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMS5zZXJ2 -ZXIuY2EuY3J0MCMGA1UdEgQcMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzAN -BgkqhkiG9w0BAQUFAAOCAQEAfhP/++WewlphCojZwXijpFIy+XX1gR0p4kSfxVgA -Anl3khFL/xAvhk6pbWjGQM/9FWb/PFDRgj4fvMKGR8F9bMKNfBOrT+SyWDuI1Ax3 -y0unu0vZjEfUJmMktrr2aN3NI/bBmdVixNntsHRB0yrrl7Zk0TjQM3I1egfygoxa -tfARn5QOO/sReYJXlJdwmFMH0dpMT3++p5RhMZPDVeAdUUK/KzSdlPkVrLPJTKEY -d+IAIWjZq5CGOjM9052+CDhyAMvdywJQpxuhO/BzmPrt0ZQwuMdTUutPT2ijDGCB -J7nCUCVEtF25KJrJxeXY6oLxgXoaqqU1ZGivAS1oCtnocg== ------END CERTIFICATE----- \ No newline at end of file diff --git a/conf/certs/quay-staging.key b/conf/certs/quay-staging.key deleted file mode 100644 index 2b41c1c6e..000000000 --- a/conf/certs/quay-staging.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA6uOT6AUsgJ4ooJV8reciN5Gy3MC10U61jQr9K8YvuexJcbP4 -se0SCCINYjtRQEST/4qUURxQfBjbhvniHSQBGngd/Xso7xr7DnBPIZStR7tx3dz3 -CgMGlSaVWck25VGF8ueZMPaJMDWSaVSTzh8SmjozWKycp8PdlSdtVnnhpPpIdBGS -kOL7abDDGCCWqUJNQmA3a3maANExBmYsSRvJWlH1/kvgiX/SmO6h/C3n4yvX6+se -0fqneB74+pcLRleVBAZBrVNm80wDkhDDiQXMTn6z6LxRS1b7W2AkQCGNmOTz71dV -p2vbAYedocUYaCacFN/4Pi14SelBBhTjL32voQIDAQABAoIBAGW2VIblLqcnVZps -AQhhDQ0ZF2XGQTU4qx8/QfAhqusMqaUF9Mw/R06kSD1gSEfXKms+vAj/hM6oCO/C -5yoNPDkVCI+KNGiNu2c+NNXqxrpILf+Pvp3kP4Z4pbWyjwXwLlvH9Csiprdsi1D3 -IeXgyLJmP3PHkzKGez4qS4tlzdMdBbJkdCQiE35yyF2os7F4HbehQ2Qyfw8PZk9o -T8uUEyh7SjYqmxJ2GfGXQd7+NXb3S1j7ehk/XTzlxgkhMW+eWk4hRAhd4j7FICAD -0UYx9K/j2TP8tNHgNd0k/BZkIbic6FD09YagPRu71Tc7MPvcSPm9SMDOj3WXJNC9 -/oDsOEECgYEA+w6mPpPxlspaEPsCYNo1/FdnmOYecLhruzqUU6lbSBhzW6p0R7H2 -GmfLeE4mGvkPJbx/zU13sRRhtzRB7QjuzZhKgrO8c/UoJeSaFimI3NYwjhtvszU6 -ActNQOpq5WvBXEOi+FegNW5+6vTnmxGG+gj8nSsu0JBPc2Db6jeFQbkCgYEA74Nw -X/iqRtuz+yabc4ALyowHJHdI5FUHv8uPv9Fk9KVRvxwq8Ak0ZcWnO0Sc+0eUC/v+ -VDVSvf1O+pMli+zIoAzGmLQFt/Is3E4frbBI7D3tWFjCzyduyVNbQBwhzfCDjk7z -Xr/vQ1tLlll5QhABtUdJlWIvZFRfm1Qi/un/8SkCgYAaui+Gn/drRzWZcy+IohJ3 -P9LemzkIZQnLD+x0j6YRIdE+JAJnE5IQs5Ycw60Y2AT9zniIocOpTXMtrtmJ45aQ -urLMAViBu8q/ZfvlehyA7iiTKGaW3IbFZCBgVdR1gig+q1CxQZrjtVS7rMDvaElH -WyeRj+RW/dYHgXtIDwsXuQKBgQDNYCOH5636vIGEJgK981opABFPz4kNYWwXpfFJ -RcAPl4KVIQ4gbYQkkGtpgtgpD6N+80GN63tbtk12x542cX9G3i2c2yDcLikRb1vy -j2q4SBGw48uH3gQ9VeC1BGpoMIheCozc/i+nzizuayJy507PpqUOFvcUTNT+WBL6 -CqSQoQKBgQCAtGhhTn35n/MlNKSrJ6mo8uKmkjX04trlusE+Ub9J2Bb+MT+Vdy/h -vsxAJWH9xcTPr6fTGmsiHBP2q8HzU36CFJPsk8mLnnT2Q5rd4nc3KMmXGu/NUwxA -AzNbRNJ45O+BZfm6YfZUXhHq4YmNauNwoWzAWmdP1701xI/1Jtmfeg== ------END RSA PRIVATE KEY----- diff --git a/conf/certs/quay-unified.cert b/conf/certs/quay-unified.cert deleted file mode 100644 index 2d94e9306..000000000 --- a/conf/certs/quay-unified.cert +++ /dev/null @@ -1,121 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIHRjCCBi6gAwIBAgIDDDb4MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ -TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg -MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMwOTMwMTUxMTI3 -WhcNMTQxMDAxMDYyMDAxWjBhMRkwFwYDVQQNExBlNEZTNTBhYmNYcmQyZnlJMQsw -CQYDVQQGEwJVUzEUMBIGA1UEAxMLd3d3LnF1YXkuaW8xITAfBgkqhkiG9w0BCQEW -Emhvc3RtYXN0ZXJAcXVheS5pbzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBANGOItO9zOeJES+cQjB/8scbkLghi8wIvFnw/VJUUYsFrRYF2PJ96nrd0hcM -te/cvlU9phw6zhlay1zb8OuIAhtgIYFcKw/t41F7DRZGj+JaT620D5jFebWgLbLf -pxWnqGfGR4x5XgZOvzpWUgFBnX+KzvzwqfZndRLBBjpq2Rau30zggS6ff2iUNwPZ -8vPHUv/RQ6XVzq0WtbJQ1B3KVwSwcd9Eclg15LrWBd6RQxIl84CYDO6vhl00D6C8 -x8lvTjW+nB8mnnGS4F8pa3i5euwCMXWepO8EFGpeK4QikOFTevYAx1BUHeE/MGJX -FfPVIjhFVzWSrCnE2YjUcUAYoOnv0ZltpBFgsPUKyWZ4ZN3vbToorm4OYu9SJYtJ -FP51OsTizuyC85hm9zA03D3pf7zOIwIWwTG2ZdmKW4g3gNt8EJv25QC9vSiPmLa4 -wWzHgeRiMc7W9+lEive7HDafVBZQ3DX05qRbsYijhXTW6iojw0YntP5o3ndK/9Id -WfuP0cQxwxtAy7ykmnPUZ0ES58Hmf63QQ+unWhqO2nfbw/741/zC+ryyf0hcJmac -lS0Yjnisk4R62MOiRzyYxw0h8UBHBJvAzsNi+ouLtkEm8F8ne6wawGcXixwHPQnc -52XCcYZsguVwa5Pohh6/rcisTTJ3P9NSouFw4l2ghcrbwPALAgMBAAGjggLZMIIC -1TAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAd -BgNVHQ4EFgQUkty8z9tltZ1SV8qVzUHTjRewBwswHwYDVR0jBBgwFoAU60I00Jiw -q5/0G2sI98xkLu8OLEUwHwYDVR0RBBgwFoILd3d3LnF1YXkuaW+CB3F1YXkuaW8w -ggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgEwggE7BgsrBgEEAYG1NwECAzCCASow -LgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYw -gfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3JkaW5n -IHRvIHRoZSBDbGFzcyAxIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRoZSBT -dGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRlbmRl -ZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkgb2Js -aWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRzc2wu -Y29tL2NydDEtY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcwAYYt -aHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczEvc2VydmVyL2NhMEIG -CCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIuY2xh -c3MxLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFydHNz -bC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQAFwzBHJ7d/Lutu/ub6gSDdMDvwAmWp -gsspoEqJJwFLPiVC3s7Ejj2qM20t2FZ4hEHPoQlfqQa/AmxQCLQnY5EnfJLZawwn -dYCFAbkGWvxa11ROHHv5gBovltBGef9lHuNzIc9hkkSpUBzH+nakGAdMs5MD+NH1 -f6GnsIOXLWlB14WHFSyN/v/bTZGkoq/9Tgkl4v4AkoZz6Fpd1XmZ4EXrZVBW/wWc -vnhKe3Khx2xU5xlU0GdtAd7WhasZhzyy7OxUvLqlnJygBYTg2R7RYF8jF3r85PU+ -NPo9t8Xl8I+y7JiD10WRJQdd34yK6DJlmr358J0nWWk7PQv6p5Cpx475 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGNDCCBBygAwIBAgIBGDANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg -Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjA1NDE3WhcNMTcxMDI0MjA1NDE3WjCB -jDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsT -IlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0 -YXJ0Q29tIENsYXNzIDEgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtonGrO8JUngHrJJj0PREGBiE -gFYfka7hh/oyULTTRwbw5gdfcA4Q9x3AzhA2NIVaD5Ksg8asWFI/ujjo/OenJOJA -pgh2wJJuniptTT9uYSAK21ne0n1jsz5G/vohURjXzTCm7QduO3CHtPn66+6CPAVv -kvek3AowHpNz/gfK11+AnSJYUq4G2ouHI2mw5CrY6oPSvfNx23BaKA+vWjhwRRI/ -ME3NO68X5Q/LoKldSKqxYVDLNM08XMML6BDAjJvwAwNi/rJsPnIO7hxDKslIDlc5 -xDEhyBDBLIf+VJVSH1I8MRKbf+fAoKVZ1eKPPvDVqOHXcDGpxLPPr21TLwb0pwID -AQABo4IBrTCCAakwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFOtCNNCYsKuf9BtrCPfMZC7vDixFMB8GA1UdIwQYMBaAFE4L7xqkQFul -F2mHMMo0aEPQQa7yMGYGCCsGAQUFBwEBBFowWDAnBggrBgEFBQcwAYYbaHR0cDov -L29jc3Auc3RhcnRzc2wuY29tL2NhMC0GCCsGAQUFBzAChiFodHRwOi8vd3d3LnN0 -YXJ0c3NsLmNvbS9zZnNjYS5jcnQwWwYDVR0fBFQwUjAnoCWgI4YhaHR0cDovL3d3 -dy5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0 -c3NsLmNvbS9zZnNjYS5jcmwwgYAGA1UdIAR5MHcwdQYLKwYBBAGBtTcBAgEwZjAu -BggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjA0 -BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50ZXJtZWRpYXRl -LnBkZjANBgkqhkiG9w0BAQUFAAOCAgEAIQlJPqWIbuALi0jaMU2P91ZXouHTYlfp -tVbzhUV1O+VQHwSL5qBaPucAroXQ+/8gA2TLrQLhxpFy+KNN1t7ozD+hiqLjfDen -xk+PNdb01m4Ge90h2c9W/8swIkn+iQTzheWq8ecf6HWQTd35RvdCNPdFWAwRDYSw -xtpdPvkBnufh2lWVvnQce/xNFE+sflVHfXv0pQ1JHpXo9xLBzP92piVH0PN1Nb6X -t1gW66pceG/sUzCv6gRNzKkC4/C2BBL2MLERPZBOVmTX3DxDX3M570uvh+v2/miI -RHLq0gfGabDBoYvvF0nXYbFFSF87ICHpW7LM9NfpMfULFWE7epTj69m8f5SuauNi -YpaoZHy4h/OZMn6SolK+u/hlz8nyMPyLwcKmltdfieFcNID1j0cHL7SRv7Gifl9L -WtBbnySGBVFaaQNlQ0lxxeBvlDRr9hvYqbBMflPrj0jfyjO1SPo2ShpTpjMM0InN -SRXNiTE8kMBy12VLUjWKRhFEuT2OKGWmPnmeXAhEKa2wNREuIU640ucQPl2Eg7PD -wuTSxv0JS3QJ3fGz0xk+gA2iCxnwOOfFwq/iI9th4p1cbiCJSS4jarJiwUW0n6+L -p/EiO/h94pDQehn7Skzj0n1fSoMD7SfWI55rjbRZotnvbIIp3XUZPD9MEI3vu3Un -0q6Dp6jOW6c= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg -Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 -MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi -U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh -cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk -pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf -OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C -Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT -Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi -HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM -Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w -+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ -Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 -Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B -26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID -AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE -FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j -ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js -LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM -BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 -Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy -dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh -cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh -YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg -dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp -bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ -YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT -TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ -9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 -jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW -FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz -ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 -ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L -EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu -L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq -yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC -O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V -um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh -NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= ------END CERTIFICATE----- diff --git a/conf/certs/quay.cert b/conf/certs/quay.cert deleted file mode 100644 index 2ec5360b6..000000000 --- a/conf/certs/quay.cert +++ /dev/null @@ -1,41 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIHRjCCBi6gAwIBAgIDDDb4MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ -TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg -MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMwOTMwMTUxMTI3 -WhcNMTQxMDAxMDYyMDAxWjBhMRkwFwYDVQQNExBlNEZTNTBhYmNYcmQyZnlJMQsw -CQYDVQQGEwJVUzEUMBIGA1UEAxMLd3d3LnF1YXkuaW8xITAfBgkqhkiG9w0BCQEW -Emhvc3RtYXN0ZXJAcXVheS5pbzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBANGOItO9zOeJES+cQjB/8scbkLghi8wIvFnw/VJUUYsFrRYF2PJ96nrd0hcM -te/cvlU9phw6zhlay1zb8OuIAhtgIYFcKw/t41F7DRZGj+JaT620D5jFebWgLbLf -pxWnqGfGR4x5XgZOvzpWUgFBnX+KzvzwqfZndRLBBjpq2Rau30zggS6ff2iUNwPZ -8vPHUv/RQ6XVzq0WtbJQ1B3KVwSwcd9Eclg15LrWBd6RQxIl84CYDO6vhl00D6C8 -x8lvTjW+nB8mnnGS4F8pa3i5euwCMXWepO8EFGpeK4QikOFTevYAx1BUHeE/MGJX -FfPVIjhFVzWSrCnE2YjUcUAYoOnv0ZltpBFgsPUKyWZ4ZN3vbToorm4OYu9SJYtJ -FP51OsTizuyC85hm9zA03D3pf7zOIwIWwTG2ZdmKW4g3gNt8EJv25QC9vSiPmLa4 -wWzHgeRiMc7W9+lEive7HDafVBZQ3DX05qRbsYijhXTW6iojw0YntP5o3ndK/9Id -WfuP0cQxwxtAy7ykmnPUZ0ES58Hmf63QQ+unWhqO2nfbw/741/zC+ryyf0hcJmac -lS0Yjnisk4R62MOiRzyYxw0h8UBHBJvAzsNi+ouLtkEm8F8ne6wawGcXixwHPQnc -52XCcYZsguVwa5Pohh6/rcisTTJ3P9NSouFw4l2ghcrbwPALAgMBAAGjggLZMIIC -1TAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAd -BgNVHQ4EFgQUkty8z9tltZ1SV8qVzUHTjRewBwswHwYDVR0jBBgwFoAU60I00Jiw -q5/0G2sI98xkLu8OLEUwHwYDVR0RBBgwFoILd3d3LnF1YXkuaW+CB3F1YXkuaW8w -ggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgEwggE7BgsrBgEEAYG1NwECAzCCASow -LgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYw -gfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3JkaW5n -IHRvIHRoZSBDbGFzcyAxIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRoZSBT -dGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRlbmRl -ZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkgb2Js -aWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRzc2wu -Y29tL2NydDEtY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcwAYYt -aHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczEvc2VydmVyL2NhMEIG -CCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIuY2xh -c3MxLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFydHNz -bC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQAFwzBHJ7d/Lutu/ub6gSDdMDvwAmWp -gsspoEqJJwFLPiVC3s7Ejj2qM20t2FZ4hEHPoQlfqQa/AmxQCLQnY5EnfJLZawwn -dYCFAbkGWvxa11ROHHv5gBovltBGef9lHuNzIc9hkkSpUBzH+nakGAdMs5MD+NH1 -f6GnsIOXLWlB14WHFSyN/v/bTZGkoq/9Tgkl4v4AkoZz6Fpd1XmZ4EXrZVBW/wWc -vnhKe3Khx2xU5xlU0GdtAd7WhasZhzyy7OxUvLqlnJygBYTg2R7RYF8jF3r85PU+ -NPo9t8Xl8I+y7JiD10WRJQdd34yK6DJlmr358J0nWWk7PQv6p5Cpx475 ------END CERTIFICATE----- \ No newline at end of file diff --git a/conf/certs/quay.key b/conf/certs/quay.key deleted file mode 100644 index 9b24c7482..000000000 --- a/conf/certs/quay.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKgIBAAKCAgEA0Y4i073M54kRL5xCMH/yxxuQuCGLzAi8WfD9UlRRiwWtFgXY -8n3qet3SFwy179y+VT2mHDrOGVrLXNvw64gCG2AhgVwrD+3jUXsNFkaP4lpPrbQP -mMV5taAtst+nFaeoZ8ZHjHleBk6/OlZSAUGdf4rO/PCp9md1EsEGOmrZFq7fTOCB -Lp9/aJQ3A9ny88dS/9FDpdXOrRa1slDUHcpXBLBx30RyWDXkutYF3pFDEiXzgJgM -7q+GXTQPoLzHyW9ONb6cHyaecZLgXylreLl67AIxdZ6k7wQUal4rhCKQ4VN69gDH -UFQd4T8wYlcV89UiOEVXNZKsKcTZiNRxQBig6e/RmW2kEWCw9QrJZnhk3e9tOiiu -bg5i71Ili0kU/nU6xOLO7ILzmGb3MDTcPel/vM4jAhbBMbZl2YpbiDeA23wQm/bl -AL29KI+YtrjBbMeB5GIxztb36USK97scNp9UFlDcNfTmpFuxiKOFdNbqKiPDRie0 -/mjed0r/0h1Z+4/RxDHDG0DLvKSac9RnQRLnweZ/rdBD66daGo7ad9vD/vjX/ML6 -vLJ/SFwmZpyVLRiOeKyThHrYw6JHPJjHDSHxQEcEm8DOw2L6i4u2QSbwXyd7rBrA -ZxeLHAc9CdznZcJxhmyC5XBrk+iGHr+tyKxNMnc/01Ki4XDiXaCFytvA8AsCAwEA -AQKCAgEAmoxnZx5eFoziXeiycC6NEQdlbkdfYPU4ZGT1j1icYxmmk81wOTdgTYl3 -PoSjUenNffRfpAZCpjRuM2gKgMroMuRtEYi2QaNCuX81Ia6cw2Wzyfo4XoWVw7wE -uB12juP9sbtsXU/NZn2BTzcGd+K6k6v+CFI+J3oZv+EYBNF0leQW0A3reEUtpCVb -hb2iDuR8dCsT5ySOrt1G1+IA7o+iKdUvxmgmpKPqs1jRL1qWyrWuprJ9JzPQtsCE -nhlch1VNqxmO7vJ+fGjEjapwlrLE03ayn3qHTbgGjoQxN9x+WZBF6VSdqsK+3rbJ -ql1r1U7lU/bf4KTx0ERb4yw68fi4Azldo+KxIP6kVPPItgwTH2q5H4j+Xqh9ok8e -szM7y8XG/EY/uHup/34HjKuAiDE9/weWHQmG8/tOvleNl2YLv4bEvzechnJ6lvfm -Ky4uzf+yCU3zhsvVsd7zEaNy+0VefOyx0B7eQojadJlrhaUL8YGwnTDOACO5JcHg -5V/1PQN9XOvUF5rB/NJKeHmPJKRumTtin/jmpsgAhZUhK7WBGYHKrGRlEnqTOzVs -64mHqi2jCjt01VfguzymDbbX/HDE2Q5kHWUhxMjLb3zOsMzQy4dUw9AUdkvEiGOW -Yni47vmjbSbIxUBjgpYcr4NBzwXfJlr5XffBobTykMaHqHLq2oECggEBAP0TMicH -fuIAqfF1jVTiAhlryrGrCMeP3PeTmS6QJI5JncAelB1b6GdT8ptLhjsJPHaaXmuD -M5GlUmjfVH57SJnDo8ORhNCxzfmXfRFezHkRuXIWC9D56I/cV1W4NCvIddKhDODQ -mxsWTsxhFvjcPSdAEQVIbM+LcAo2RfvAQc5i7WHbjpjpOVL9fvwW+81WXl0oewly -sXM5KTZoS7j+I539CEeIFzRD4EnlgIEeM0DCpT1A4IxxdnteRMxKetE8J3LmvxX8 -FPyFuIMFMNkrr3bgVrcVXnDjzE26ciAMsKUQwytboGB9oFL/kh/AkJmToPzrh98W -z7N1kVZN5ErBOuECggEBANP6LEAHK7vbH3HayvNZcWYT2u4JRsvRSa/5zCDRBRIX -/IN0nkrdK9HL4dEhLN9hUxdNdrmTEUB7B4FaDk9yP8pDiAIyhLvl69g3IIaGpuHJ -N6j+V1lw39UaKcNF05Cn+N0mkvOTA9gUlO4N0bg1O7cV7Qq4qd5tDCGI3DLqrrQK -nfFcvgkhXPs97EZt61GQLCKp8X6nnrIPuz0PJ9TeNGe5izmVayNrkDGmfaXS1vFf -Kf90nkrDW95v0HvKgfoXsnkNqp6aZrxin8iwJOjiFHXC5xy3C1G2/68CJx49EI+L -95qRp5d+f3HlDRNBeVPUCfNt729avaD4irFJGTKb1GsCggEBAJxDCAqVVEEUC9rt -rJCm5Ijxx7wgUVF3gQbVehYIJqo8xkzkFKx0HXH/oaNF5OH69/x7oKVd46+gltvu -WeunD9Lxu+J7rbh2sSnV4gGhuTtgOFM7TZyBUpnRgZOKI5yNMEMX8i22YK8+/PSx -Vk/fHVto8ZmDeLxF6q5DiL7DnV5kMxLjUI8WIrEdmRTq1BuborR+1EmnKe5tcwcH -KwpU0YUxwbT5UOqSpqC6NriC/z6TcRf7QSs2u+O891n0+xTKwcjutTpL5mFt59nu -kJTnpnYOWzy80w19ep7b6q/jZgbl7LyO3N33c9ELwRwd+Kr7PsIsZD7ZhPHYPB9A -BSArY8ECggEAOVCTVlyZ/pkoz2gRJ+svNiJ4N5RaiBF2kxY/kz/w1wuVQxXtFuDm -UDuIOzt1HpD6HnrbdyHEsGKTjO4EoIaLqOzJgY6XRRbNxhBhwv31cWcunYrno09Z -tgz07c+bfKluKJ2dbi56A5rNCfDCm8QI+V/8T8HObE2f0hFnOH0r75JPUkt5No7G -zUfY8tIVpmANDvJUUaKQziRixAetBWlvUfxhIJi99z3GJyaVIpj3dRv+BwxJIH3i -ASrKfC4tJqnxn7mKQIgO7zDbcy/tSuqWDaE9TA5SCS4pw/AZE5v/NlDqCekIH3Yc -j3cXKfWyEHBsoF1BOCKY00Vger8BCSYJMQKCAQEA0dpZUfLed/v8MAoRANfV125j -Y5RUKt4qvRYdx97HnXStHokOM1RO3mT4OfiTX516k5VUu72vtNQLCnsovjG8U9Wd -H+HqWfDT/rVzTnW/t2Wpg1JadUcJwWVEpRsCua9mdJCr694wKsD/vCDSFiOxsJJC -N6292d3Il/WOZqDgUwhKJ6VTYVDXvYae6zbUsF/JVaTG3p42wqae+qWNxIbT1Dtq -PxBol7YvCXKeJYE3AITPrn77+KjTZfURNbi/ICNN4Vl+M62pbYsl79l2dgRKmM8w -SHjF+OT6d1i5MmDk3dBwvIB+xLLkizA7+Ed5Cc2X0e3q2ZyYQBV03slkBx9qzA== ------END RSA PRIVATE KEY----- diff --git a/conf/cloud-init.sh b/conf/cloud-init.sh deleted file mode 100755 index e3a69c302..000000000 --- a/conf/cloud-init.sh +++ /dev/null @@ -1,60 +0,0 @@ -#! /bin/sh - -apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 -sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" -apt-get update - -apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core lxc-docker - -PRIVATE_KEY=/root/.ssh/id_rsa -echo '-----BEGIN RSA PRIVATE KEY-----' > $PRIVATE_KEY -echo 'MIIEpAIBAAKCAQEA1qYjqPJAOHzE9jyE06LgOYFXtmVWMMPdS10oWUH77/M406/l' >> $PRIVATE_KEY -echo 'BG1Nf8VU2/q7VogfR/k56xumlAYcoEP9rueEMI9j2RwDy2s5SHaT7Z+9SyZnTRtq' >> $PRIVATE_KEY -echo 'bomTUHVBQtxgRXz2XHROWtFG54MhZtIHDk31kW2qyr+rMw2/kT1h6+s9D1mF5A5i' >> $PRIVATE_KEY -echo 'DWxNQSWYyS9gaM5a5aNUVscoXAtSG7JwY4XdYEGKXwMm7UYFeHlOPH/QRTZVO9XP' >> $PRIVATE_KEY -echo 'Z/vNW1t6JZ9GIAxfFP9v2YyehF3l2R+m3VGDld4JNosUPyWOnMPbHBcTYGe2nLgj' >> $PRIVATE_KEY -echo 'zH9mqhXKR0jR2hbo0QJz5ln8TXmj5v3mfPrF1QIDAQABAoIBAC52Y/2sAm63w0Kx' >> $PRIVATE_KEY -echo 'subEuNh5wOzAXrnLi9lGXveDKu+zrDdWObKNnlrr8gRz7505ddv0fK8BmzsrX4Lp' >> $PRIVATE_KEY -echo 'dL4paxm/0BMs1z1vBkVDNZ4YF7dupqmwJ4epy/N8jhXU8hnYhNNacaOC7WArqE1D' >> $PRIVATE_KEY -echo 'ZTeZdHB4VqHwfzRb432i1dFlaCAsEQ+pRg+o0wOqH5BMZy4LY5vESK5d2E85KhqT' >> $PRIVATE_KEY -echo '1rgD2T2FrkM42H4QvYzn6ntmjRAA5eO6RSeyPlkpniNTlmSuNYt8iqx8bm1HgXFn' >> $PRIVATE_KEY -echo 'Iova/9MifFt9CFG5SJPmYkPYvAEhNmiRdob68a/0BIX+Uuc1skX72Lpb/XjqrlXZ' >> $PRIVATE_KEY -echo 'UhJYALkCgYEA9fPGq9bGNWodCwplXuq5ydZv1BK5NZod+H85hUOz+gUN12UJ3Euy' >> $PRIVATE_KEY -echo 'FAZZqV5kwQ0i1cE6Vfg9SSk1V9osdw3TIVZgTOBKBYxsuCJzIO4zlyM7qi0XFsam' >> $PRIVATE_KEY -echo 'ax/v/kfHFnoBOPruJs0Ao5F4cGhZBfS4dQZAh4EqplSjJuGoLVMbNTsCgYEA32r8' >> $PRIVATE_KEY -echo 'kspbaCK71hDc2vAxVpHR3UNSui6lQCKOC4BbA8c1XP08+BKPONeNMaytXiRe5Vrq' >> $PRIVATE_KEY -echo 'bXRf9GqY6zzM08If78qjgDd2cfVYPnrb8unth7Z7QbsSi5+E6Gt8cevBEQqv1n6H' >> $PRIVATE_KEY -echo 'jzLKlETL5qpMpRHJi98AvyHcSpYyI6XORZE0AC8CgYEAwJJDPq5l+NKBtPBJ2Jxu' >> $PRIVATE_KEY -echo 'JUN5wZF7ZCWsS7HJZrdQxnSIltpscwjtgFJMh5j5yFGxsa2eMEuyKINUWdngMMMp' >> $PRIVATE_KEY -echo 'SRPpSKfgLSH6yd1nSSRYToDuqVqulk2pZXzXGsA2eDnElUmbh9PBKVCv/UsmUMyA' >> $PRIVATE_KEY -echo 'VFg11CLlMuBX8gyC8iH8zpsCgYB2NxDfxuzoxAApu5Bw1Ej26n9mGTpLw2Sy89W/' >> $PRIVATE_KEY -echo 'JjKCZETLKD+7b26TABL4psqxFoOTzjBerAYduM2jIu+qWHw3kDxFGpO0psIDhVSe' >> $PRIVATE_KEY -echo 'SsLhXWAInqiockaMCFu3l6v3jXUPBLJLxe9E1sYhDhkx+qBvPxcRCySZ3rE3BYOI' >> $PRIVATE_KEY -echo 'cdVXBwKBgQD1Wp1eLdnA3UV2KzyyVG3K/FqKszte70NfR9gvl6bD8cGeoAAt+iyW' >> $PRIVATE_KEY -echo 'Wd3tc3FKcDfywRoxrc4Atew0ySZVv78E2vDiyAswMhsbdldALw0sftaTIfdkXzlO' >> $PRIVATE_KEY -echo '77cUl9A2niF4mf0b8JeIGrTR81f3Q/ZRjzXMg/dZLVMtzPsFd9clGw==' >> $PRIVATE_KEY -echo '-----END RSA PRIVATE KEY-----' >> $PRIVATE_KEY -chmod 600 $PRIVATE_KEY - -BITBUCKET=AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw== -KNOWN_HOSTS=/root/.ssh/known_hosts -echo "|1|7Yac4eoTmXJj7g7Hdlz0PdJMNnQ=|5AckfCb6pvVav45AOBMStvCVwFk= ssh-rsa $BITBUCKET" >> $KNOWN_HOSTS -echo "|1|epKB6bDLmj4UCWcN2lJ9NT+WjS4=|MThQkD3gLXsDEdRGD15uBlI6j5Q= ssh-rsa $BITBUCKET" >> $KNOWN_HOSTS -echo "|1|tET4d+sodv8Zk+m/JXHj3OWpyUU=|8lo5vpeKH6yiflQpV+aNEsSZBtw= ssh-rsa $BITBUCKET" >> $KNOWN_HOSTS - -export USER=ubuntu - -git clone git@bitbucket.org:yackob03/quay.git /home/$USER/quay -cd /home/$USER/quay -virtualenv --distribute venv -venv/bin/pip install -r requirements.txt -gdebi --n binary_dependencies/*.deb -cp conf/logrotate/* /etc/logrotate.d/ -chown -R $USER:$USER /home/$USER/quay - -mkdir -p /mnt/logs/ && chown $USER /mnt/logs/ && /usr/local/nginx/sbin/nginx -c `pwd`/conf/nginx.conf -mkdir -p /mnt/logs/ && chown $USER /mnt/logs/ && STACK=prod sudo -u $USER -E venv/bin/gunicorn -c conf/gunicorn_config.py application:application - -echo '{"https://quay.io/v1/": {"auth": "cXVheStkZXBsb3k6OVkxUFg3RDNJRTRLUFNHQ0lBTEgxN0VNNVYzWlRNUDhDTk5ISk5YQVEyTkpHQVM0OEJESDhKMVBVT1o4NjlNTA==", "email": ""}}' > /root/.dockercfg -docker pull quay.io/quay/logstash -docker run -d -e REDIS_PORT_6379_TCP_ADDR=logs.quay.io -v /mnt/logs:/mnt/logs quay.io/quay/logstash quay.conf diff --git a/conf/deploy b/conf/deploy deleted file mode 100644 index 72bc1265a..000000000 --- a/conf/deploy +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1qYjqPJAOHzE9jyE06LgOYFXtmVWMMPdS10oWUH77/M406/l -BG1Nf8VU2/q7VogfR/k56xumlAYcoEP9rueEMI9j2RwDy2s5SHaT7Z+9SyZnTRtq -bomTUHVBQtxgRXz2XHROWtFG54MhZtIHDk31kW2qyr+rMw2/kT1h6+s9D1mF5A5i -DWxNQSWYyS9gaM5a5aNUVscoXAtSG7JwY4XdYEGKXwMm7UYFeHlOPH/QRTZVO9XP -Z/vNW1t6JZ9GIAxfFP9v2YyehF3l2R+m3VGDld4JNosUPyWOnMPbHBcTYGe2nLgj -zH9mqhXKR0jR2hbo0QJz5ln8TXmj5v3mfPrF1QIDAQABAoIBAC52Y/2sAm63w0Kx -subEuNh5wOzAXrnLi9lGXveDKu+zrDdWObKNnlrr8gRz7505ddv0fK8BmzsrX4Lp -dL4paxm/0BMs1z1vBkVDNZ4YF7dupqmwJ4epy/N8jhXU8hnYhNNacaOC7WArqE1D -ZTeZdHB4VqHwfzRb432i1dFlaCAsEQ+pRg+o0wOqH5BMZy4LY5vESK5d2E85KhqT -1rgD2T2FrkM42H4QvYzn6ntmjRAA5eO6RSeyPlkpniNTlmSuNYt8iqx8bm1HgXFn -Iova/9MifFt9CFG5SJPmYkPYvAEhNmiRdob68a/0BIX+Uuc1skX72Lpb/XjqrlXZ -UhJYALkCgYEA9fPGq9bGNWodCwplXuq5ydZv1BK5NZod+H85hUOz+gUN12UJ3Euy -FAZZqV5kwQ0i1cE6Vfg9SSk1V9osdw3TIVZgTOBKBYxsuCJzIO4zlyM7qi0XFsam -ax/v/kfHFnoBOPruJs0Ao5F4cGhZBfS4dQZAh4EqplSjJuGoLVMbNTsCgYEA32r8 -kspbaCK71hDc2vAxVpHR3UNSui6lQCKOC4BbA8c1XP08+BKPONeNMaytXiRe5Vrq -bXRf9GqY6zzM08If78qjgDd2cfVYPnrb8unth7Z7QbsSi5+E6Gt8cevBEQqv1n6H -jzLKlETL5qpMpRHJi98AvyHcSpYyI6XORZE0AC8CgYEAwJJDPq5l+NKBtPBJ2Jxu -JUN5wZF7ZCWsS7HJZrdQxnSIltpscwjtgFJMh5j5yFGxsa2eMEuyKINUWdngMMMp -SRPpSKfgLSH6yd1nSSRYToDuqVqulk2pZXzXGsA2eDnElUmbh9PBKVCv/UsmUMyA -VFg11CLlMuBX8gyC8iH8zpsCgYB2NxDfxuzoxAApu5Bw1Ej26n9mGTpLw2Sy89W/ -JjKCZETLKD+7b26TABL4psqxFoOTzjBerAYduM2jIu+qWHw3kDxFGpO0psIDhVSe -SsLhXWAInqiockaMCFu3l6v3jXUPBLJLxe9E1sYhDhkx+qBvPxcRCySZ3rE3BYOI -cdVXBwKBgQD1Wp1eLdnA3UV2KzyyVG3K/FqKszte70NfR9gvl6bD8cGeoAAt+iyW -Wd3tc3FKcDfywRoxrc4Atew0ySZVv78E2vDiyAswMhsbdldALw0sftaTIfdkXzlO -77cUl9A2niF4mf0b8JeIGrTR81f3Q/ZRjzXMg/dZLVMtzPsFd9clGw== ------END RSA PRIVATE KEY----- diff --git a/conf/deploy.pub b/conf/deploy.pub deleted file mode 100644 index 27165697f..000000000 --- a/conf/deploy.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWpiOo8kA4fMT2PITTouA5gVe2ZVYww91LXShZQfvv8zjTr+UEbU1/xVTb+rtWiB9H+TnrG6aUBhygQ/2u54Qwj2PZHAPLazlIdpPtn71LJmdNG2puiZNQdUFC3GBFfPZcdE5a0UbngyFm0gcOTfWRbarKv6szDb+RPWHr6z0PWYXkDmINbE1BJZjJL2Bozlrlo1RWxyhcC1IbsnBjhd1gQYpfAybtRgV4eU48f9BFNlU71c9n+81bW3oln0YgDF8U/2/ZjJ6EXeXZH6bdUYOV3gk2ixQ/JY6cw9scFxNgZ7acuCPMf2aqFcpHSNHaFujRAnPmWfxNeaPm/eZ8+sXV jake@coreserver diff --git a/conf/gunicorn_config.py b/conf/gunicorn_config.py index c7332fe96..b86250125 100644 --- a/conf/gunicorn_config.py +++ b/conf/gunicorn_config.py @@ -2,9 +2,6 @@ bind = 'unix:/tmp/gunicorn.sock' workers = 8 worker_class = 'gevent' timeout = 2000 -daemon = True -pidfile = '/mnt/logs/gunicorn.pid' -errorlog = '/mnt/logs/application.log' -loglevel = 'debug' -logger_class = 'util.glogger.LogstashLogger' +pidfile = '/tmp/gunicorn.pid' +logconfig = 'conf/logging.conf' pythonpath = '.' \ No newline at end of file diff --git a/conf/gunicorn_local.py b/conf/gunicorn_local.py index 2a145fd98..9f93eb008 100644 --- a/conf/gunicorn_local.py +++ b/conf/gunicorn_local.py @@ -3,7 +3,5 @@ workers = 2 worker_class = 'gevent' timeout = 2000 daemon = False -errorlog = '-' -loglevel = 'debug' -logger_class = 'util.glogger.LogstashLogger' +logconfig = 'conf/logging_local.conf' pythonpath = '.' \ No newline at end of file diff --git a/conf/init/diffsworker.sh b/conf/init/diffsworker.sh new file mode 100755 index 000000000..68d3c38b4 --- /dev/null +++ b/conf/init/diffsworker.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +echo 'Starting diffs worker' + +cd / +venv/bin/python -m workers.diffsworker --log=/mnt/logs/diffsworker.log + +echo 'Diffs worker exited' \ No newline at end of file diff --git a/conf/init/gunicorn.sh b/conf/init/gunicorn.sh new file mode 100755 index 000000000..a61e7c651 --- /dev/null +++ b/conf/init/gunicorn.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +echo 'Starting gunicon' + +cd / +venv/bin/gunicorn -c conf/gunicorn_config.py application:application + +echo 'Gunicorn exited' \ No newline at end of file diff --git a/conf/init/mklogsdir.sh b/conf/init/mklogsdir.sh new file mode 100755 index 000000000..4ca2880d0 --- /dev/null +++ b/conf/init/mklogsdir.sh @@ -0,0 +1,4 @@ +#! /bin/sh + +echo 'Creating logs directory' +mkdir -p /mnt/logs \ No newline at end of file diff --git a/conf/init/nginx.sh b/conf/init/nginx.sh new file mode 100755 index 000000000..6cd4b3430 --- /dev/null +++ b/conf/init/nginx.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +echo 'Starting nginx' + +if [ -f /conf/stack/ssl.key ] +then + echo "Using HTTPS" + /usr/local/nginx/sbin/nginx -c /conf/nginx-enterprise.conf +else + echo "No SSL key provided, using HTTP" + /usr/local/nginx/sbin/nginx -c /conf/nginx-enterprise-nossl.conf +fi + +echo 'Nginx exited' \ No newline at end of file diff --git a/conf/init/webhookworker.sh b/conf/init/webhookworker.sh new file mode 100755 index 000000000..7ab6340d8 --- /dev/null +++ b/conf/init/webhookworker.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +echo 'Starting webhook worker' + +cd / +venv/bin/python -m workers.webhookworker --log=/mnt/logs/webhookworker.log + +echo 'Webhook worker exited' \ No newline at end of file diff --git a/conf/logging.conf b/conf/logging.conf new file mode 100644 index 000000000..2061a2375 --- /dev/null +++ b/conf/logging.conf @@ -0,0 +1,39 @@ +[loggers] +keys=root, gunicorn.error, gunicorn.access + +[handlers] +keys=error_file + +[formatters] +keys=generic + +[logger_application.profiler] +level=DEBUG +handlers=error_file +propagate=0 +qualname=application.profiler + +[logger_root] +level=DEBUG +handlers=error_file + +[logger_gunicorn.error] +level=INFO +handlers=error_file +propagate=1 +qualname=gunicorn.error + +[logger_gunicorn.access] +level=INFO +handlers=error_file +propagate=0 +qualname=gunicorn.access + +[handler_error_file] +class=logging.FileHandler +formatter=generic +args=('/mnt/logs/application.log',) + +[formatter_generic] +format=%(asctime)s [%(process)d] [%(levelname)s] [%(name)s] %(message)s +class=logging.Formatter diff --git a/conf/logging_local.conf b/conf/logging_local.conf new file mode 100644 index 000000000..4023e7743 --- /dev/null +++ b/conf/logging_local.conf @@ -0,0 +1,39 @@ +[loggers] +keys=root, gunicorn.error, gunicorn.access, application.profiler + +[handlers] +keys=console + +[formatters] +keys=generic + +[logger_application.profiler] +level=DEBUG +handlers=console +propagate=0 +qualname=application.profiler + +[logger_root] +level=DEBUG +handlers=console + +[logger_gunicorn.error] +level=INFO +handlers=console +propagate=1 +qualname=gunicorn.error + +[logger_gunicorn.access] +level=INFO +handlers=console +propagate=0 +qualname=gunicorn.access + +[handler_console] +class=StreamHandler +formatter=generic +args=(sys.stdout, ) + +[formatter_generic] +format=%(asctime)s [%(process)d] [%(levelname)s] [%(name)s] %(message)s +class=logging.Formatter diff --git a/conf/nginx-local.conf b/conf/nginx-enterprise-nossl.conf similarity index 73% rename from conf/nginx-local.conf rename to conf/nginx-enterprise-nossl.conf index 0545399a0..73a9c7605 100644 --- a/conf/nginx-local.conf +++ b/conf/nginx-enterprise-nossl.conf @@ -2,17 +2,21 @@ include root-base.conf; worker_processes 2; +user root nogroup; + +daemon off; + http { include http-base.conf; server { include server-base.conf; - listen 5000 default; + listen 80 default; location /static/ { # checks for static file, if not found proxy to app - alias /home/jake/Projects/docker/quay/static/; + alias /static/; } } } diff --git a/conf/nginx-staging.conf b/conf/nginx-enterprise.conf similarity index 77% rename from conf/nginx-staging.conf rename to conf/nginx-enterprise.conf index f8fb03784..43c21b6ca 100644 --- a/conf/nginx-staging.conf +++ b/conf/nginx-enterprise.conf @@ -4,6 +4,8 @@ worker_processes 2; user root nogroup; +daemon off; + http { include http-base.conf; @@ -15,8 +17,8 @@ http { listen 443 default; ssl on; - ssl_certificate ./certs/quay-staging-unified.cert; - ssl_certificate_key ./certs/quay-staging.key; + ssl_certificate ./stack/ssl.cert; + ssl_certificate_key ./stack/ssl.key; ssl_session_timeout 5m; ssl_protocols SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; @@ -24,7 +26,7 @@ http { location /static/ { # checks for static file, if not found proxy to app - alias /root/quay/static/; + alias /static/; } } } diff --git a/conf/nginx.conf b/conf/nginx.conf deleted file mode 100644 index 896b151de..000000000 --- a/conf/nginx.conf +++ /dev/null @@ -1,30 +0,0 @@ -include root-base.conf; - -worker_processes 8; - -user nobody nogroup; - -http { - include http-base.conf; - - include hosted-http-base.conf; - - server { - include server-base.conf; - - listen 443 default; - - ssl on; - ssl_certificate ./certs/quay-unified.cert; - ssl_certificate_key ./certs/quay.key; - ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; - ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; - ssl_prefer_server_ciphers on; - - location /static/ { - # checks for static file, if not found proxy to app - alias /home/ubuntu/quay/static/; - } - } -} diff --git a/conf/root-base.conf b/conf/root-base.conf index 16a63fda0..b4b9beb90 100644 --- a/conf/root-base.conf +++ b/conf/root-base.conf @@ -1,4 +1,4 @@ -pid /mnt/logs/nginx.pid; +pid /tmp/nginx.pid; error_log /mnt/logs/nginx.error.log; events { diff --git a/config.py b/config.py index 2428a9a1e..d5fc126cb 100644 --- a/config.py +++ b/config.py @@ -1,198 +1,8 @@ -import logging -import logstash_formatter import requests import os.path -from peewee import MySQLDatabase, SqliteDatabase -from storage.s3 import S3Storage -from storage.local import LocalStorage -from data.userfiles import UserRequestFiles from data.buildlogs import BuildLogs from data.userevent import UserEventBuilder -from util import analytics - -from test.teststorage import FakeStorage, FakeUserfiles -from test import analytics as fake_analytics -from test.testlogs import TestBuildLogs - - -class FlaskConfig(object): - SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884' - JSONIFY_PRETTYPRINT_REGULAR = False - - -class FlaskProdConfig(FlaskConfig): - SESSION_COOKIE_SECURE = True - - -class MailConfig(object): - MAIL_SERVER = 'email-smtp.us-east-1.amazonaws.com' - MAIL_USE_TLS = True - MAIL_PORT = 587 - MAIL_USERNAME = 'AKIAIXV5SDGCPVMU3N4Q' - MAIL_PASSWORD = 'AhmX/vWE91uQ2RtcEKTkfNrzZehEjPNXOXeOXgQNfLao' - DEFAULT_MAIL_SENDER = 'support@quay.io' - MAIL_FAIL_SILENTLY = False - TESTING = False - - -class RealTransactions(object): - @staticmethod - def create_transaction(db): - return db.transaction() - - DB_TRANSACTION_FACTORY = create_transaction - - -class SQLiteDB(RealTransactions): - DB_NAME = 'test/data/test.db' - DB_CONNECTION_ARGS = { - 'threadlocals': True, - 'autorollback': True, - } - DB_DRIVER = SqliteDatabase - - -class FakeTransaction(object): - def __enter__(self): - return self - - def __exit__(self, exc_type, value, traceback): - pass - - -class EphemeralDB(object): - DB_NAME = ':memory:' - DB_CONNECTION_ARGS = {} - DB_DRIVER = SqliteDatabase - - @staticmethod - def create_transaction(db): - return FakeTransaction() - - DB_TRANSACTION_FACTORY = create_transaction - - -class RDSMySQL(RealTransactions): - DB_NAME = 'quay' - DB_CONNECTION_ARGS = { - 'host': 'fluxmonkeylogin.cb0vumcygprn.us-east-1.rds.amazonaws.com', - 'user': 'fluxmonkey', - 'passwd': '8eifM#uoZ85xqC^', - 'threadlocals': True, - 'autorollback': True, - } - DB_DRIVER = MySQLDatabase - - -class AWSCredentials(object): - AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA' - AWS_SECRET_KEY = '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' - REGISTRY_S3_BUCKET = 'quay-registry' - - -class S3Storage(AWSCredentials): - STORAGE = S3Storage('', AWSCredentials.AWS_ACCESS_KEY, - AWSCredentials.AWS_SECRET_KEY, - AWSCredentials.REGISTRY_S3_BUCKET) - - -class LocalStorage(object): - STORAGE = LocalStorage('test/data/registry') - - -class FakeStorage(object): - STORAGE = FakeStorage() - - -class FakeUserfiles(object): - USERFILES = FakeUserfiles() - - -class S3Userfiles(AWSCredentials): - USERFILES = UserRequestFiles(AWSCredentials.AWS_ACCESS_KEY, - AWSCredentials.AWS_SECRET_KEY, - AWSCredentials.REGISTRY_S3_BUCKET) - - -class RedisBuildLogs(object): - BUILDLOGS = BuildLogs('logs.quay.io') - - -class UserEventConfig(object): - USER_EVENTS = UserEventBuilder('logs.quay.io') - - -class TestBuildLogs(object): - BUILDLOGS = TestBuildLogs('logs.quay.io', 'devtable', 'building', - 'deadbeef-dead-beef-dead-beefdeadbeef') - - -class StripeTestConfig(object): - STRIPE_SECRET_KEY = 'sk_test_PEbmJCYrLXPW0VRLSnWUiZ7Y' - STRIPE_PUBLISHABLE_KEY = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh' - - -class StripeLiveConfig(object): - STRIPE_SECRET_KEY = 'sk_live_TRuTHYwTvmrLeU3ib7Z9hpqE' - STRIPE_PUBLISHABLE_KEY = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu' - - -class FakeAnalytics(object): - ANALYTICS = fake_analytics - - -class MixpanelTestConfig(object): - ANALYTICS = analytics - MIXPANEL_KEY = '38014a0f27e7bdc3ff8cc7cc29c869f9' - - -class MixpanelProdConfig(MixpanelTestConfig): - MIXPANEL_KEY = '50ff2b2569faa3a51c8f5724922ffb7e' - - -class GitHubTestConfig(object): - GITHUB_CLIENT_ID = 'cfbc4aca88e5c1b40679' - GITHUB_CLIENT_SECRET = '7d1cc21e17e10cd8168410e2cd1e4561cb854ff9' - GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token' - GITHUB_USER_URL = 'https://api.github.com/user' - GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails' - - -class GitHubStagingConfig(GitHubTestConfig): - GITHUB_CLIENT_ID = '4886304accbc444f0471' - GITHUB_CLIENT_SECRET = '27d8a5d99af02dda821eb10883bcb2e785e70a62' - - -class GitHubProdConfig(GitHubTestConfig): - GITHUB_CLIENT_ID = '5a8c08b06c48d89d4d1e' - GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1' - - -class DigitalOceanConfig(object): - DO_CLIENT_ID = 'LJ44y2wwYj1MD0BRxS6qHA' - DO_CLIENT_SECRET = 'b9357a6f6ff45a33bb03f6dbbad135f9' - DO_SSH_KEY_ID = '46986' - DO_SSH_PRIVATE_KEY_FILENAME = 'certs/digital_ocean' - DO_ALLOWED_REGIONS = {1, 4} - DO_DOCKER_IMAGE = 1341147 - - -class BuildNodeConfig(object): - BUILD_NODE_PULL_TOKEN = 'F02O2E86CQLKZUQ0O81J8XDHQ6F0N1V36L9JTOEEK6GKKMT1GI8PTJQT4OU88Y6G' - - -def logs_init_builder(level=logging.DEBUG, - formatter=logstash_formatter.LogstashFormatter()): - @staticmethod - def init_logs(): - handler = logging.StreamHandler() - root_logger = logging.getLogger('') - root_logger.setLevel(level) - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - return init_logs def build_requests_session(): @@ -204,70 +14,124 @@ def build_requests_session(): return sess -class LargePoolHttpClient(object): +# The set of configuration key names that will be accessible in the client. Since these +# values are set to the frontend, DO NOT PLACE ANY SECRETS OR KEYS in this list. +CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID', + 'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY', + 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN'] + + +def getFrontendVisibleConfig(config_dict): + visible_dict = {} + for name in CLIENT_WHITELIST: + if name.lower().find('secret') >= 0: + raise Exception('Cannot whitelist secrets: %s' % name) + + if name in config_dict: + visible_dict[name] = config_dict.get(name, None) + + return visible_dict + + +class DefaultConfig(object): + # Flask config + SECRET_KEY = 'a36c9d7d-25a9-4d3f-a586-3d2f8dc40a83' + JSONIFY_PRETTYPRINT_REGULAR = False + SESSION_COOKIE_SECURE = False + + LOGGING_LEVEL = 'DEBUG' + SEND_FILE_MAX_AGE_DEFAULT = 0 + POPULATE_DB_TEST_DATA = True + PREFERRED_URL_SCHEME = 'http' + SERVER_HOSTNAME = 'localhost:5000' + + # Mail config + MAIL_SERVER = '' + MAIL_USE_TLS = True + MAIL_PORT = 587 + MAIL_USERNAME = '' + MAIL_PASSWORD = '' + DEFAULT_MAIL_SENDER = '' + MAIL_FAIL_SILENTLY = False + TESTING = True + + # DB config + DB_URI = 'sqlite:///test/data/test.db' + DB_CONNECTION_ARGS = { + 'threadlocals': True, + 'autorollback': True, + } + + @staticmethod + def create_transaction(db): + return db.transaction() + + DB_TRANSACTION_FACTORY = create_transaction + + # Data storage + STORAGE_TYPE = 'LocalStorage' + STORAGE_PATH = 'test/data/registry' + + # Build logs + BUILDLOGS = BuildLogs('logs.quay.io') # Change me + + # Real-time user events + USER_EVENTS = UserEventBuilder('logs.quay.io') + + # Stripe config + BILLING_TYPE = 'FakeStripe' + + # Userfiles + USERFILES_TYPE = 'LocalUserfiles' + USERFILES_PATH = 'test/data/registry/userfiles' + + # Analytics + ANALYTICS_TYPE = "FakeAnalytics" + + # Exception logging + EXCEPTION_LOG_TYPE = 'FakeSentry' + SENTRY_DSN = None + SENTRY_PUBLIC_DSN = None + + # Github Config + GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token' + GITHUB_USER_URL = 'https://api.github.com/user' + GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails' + + GITHUB_CLIENT_ID = '' + GITHUB_CLIENT_SECRET = '' + + GITHUB_LOGIN_CLIENT_ID = '' + GITHUB_LOGIN_CLIENT_SECRET = '' + + # Requests based HTTP client with a large request pool HTTPCLIENT = build_requests_session() - -class StatusTagConfig(object): + # Status tag config STATUS_TAGS = {} - for tag_name in ['building', 'failed', 'none', 'ready']: tag_path = os.path.join('buildstatus', tag_name + '.svg') with open(tag_path) as tag_svg: STATUS_TAGS[tag_name] = tag_svg.read() + WEBHOOK_QUEUE_NAME = 'webhook' + DIFFS_QUEUE_NAME = 'imagediff' + DOCKERFILE_BUILD_QUEUE_NAME = 'dockerfilebuild' -class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles, - FakeAnalytics, StripeTestConfig, RedisBuildLogs, - UserEventConfig, LargePoolHttpClient, StatusTagConfig): - LOGGING_CONFIG = logs_init_builder(logging.WARN) - POPULATE_DB_TEST_DATA = True - TESTING = True - URL_SCHEME = 'http' - URL_HOST = 'localhost:5000' + # Super user config. Note: This MUST BE an empty list for the default config. + SUPER_USERS = [] + # Feature Flag: Whether billing is required. + FEATURE_BILLING = True -class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, - StripeTestConfig, MixpanelTestConfig, GitHubTestConfig, - DigitalOceanConfig, BuildNodeConfig, S3Userfiles, - UserEventConfig, TestBuildLogs, LargePoolHttpClient, - StatusTagConfig): - LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter()) - SEND_FILE_MAX_AGE_DEFAULT = 0 - POPULATE_DB_TEST_DATA = True - URL_SCHEME = 'http' - URL_HOST = 'ci.devtable.com:5000' + # Feature Flag: Whether user accounts automatically have usage log access. + FEATURE_USER_LOG_ACCESS = False + # Feature Flag: Whether GitHub login is supported. + FEATURE_GITHUB_LOGIN = False -class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, - StripeLiveConfig, MixpanelTestConfig, - GitHubProdConfig, DigitalOceanConfig, - BuildNodeConfig, S3Userfiles, RedisBuildLogs, - UserEventConfig, LargePoolHttpClient, - StatusTagConfig): - LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter()) - SEND_FILE_MAX_AGE_DEFAULT = 0 - URL_SCHEME = 'http' - URL_HOST = 'ci.devtable.com:5000' + # Feature flag, whether to enable olark chat + FEATURE_OLARK_CHAT = False - -class StagingConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL, - StripeLiveConfig, MixpanelProdConfig, - GitHubStagingConfig, DigitalOceanConfig, BuildNodeConfig, - S3Userfiles, RedisBuildLogs, UserEventConfig, - LargePoolHttpClient, StatusTagConfig): - LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter()) - SEND_FILE_MAX_AGE_DEFAULT = 0 - URL_SCHEME = 'https' - URL_HOST = 'staging.quay.io' - - -class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL, - StripeLiveConfig, MixpanelProdConfig, - GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig, - S3Userfiles, RedisBuildLogs, UserEventConfig, - LargePoolHttpClient, StatusTagConfig): - LOGGING_CONFIG = logs_init_builder() - SEND_FILE_MAX_AGE_DEFAULT = 0 - URL_SCHEME = 'https' - URL_HOST = 'quay.io' + # Feature Flag: Whether super users are supported. + FEATURE_SUPER_USERS = False diff --git a/data/billing.py b/data/billing.py new file mode 100644 index 000000000..8872ad87f --- /dev/null +++ b/data/billing.py @@ -0,0 +1,234 @@ +import stripe + +from datetime import datetime, timedelta +from calendar import timegm + +PLANS = [ + # Deprecated Plans + { + 'title': 'Micro', + 'price': 700, + 'privateRepos': 5, + 'stripeId': 'micro', + 'audience': 'For smaller teams', + 'bus_features': False, + 'deprecated': True, + }, + { + 'title': 'Basic', + 'price': 1200, + 'privateRepos': 10, + 'stripeId': 'small', + 'audience': 'For your basic team', + 'bus_features': False, + 'deprecated': True, + }, + { + 'title': 'Medium', + 'price': 2200, + 'privateRepos': 20, + 'stripeId': 'medium', + 'audience': 'For medium teams', + 'bus_features': False, + 'deprecated': True, + }, + { + 'title': 'Large', + 'price': 5000, + 'privateRepos': 50, + 'stripeId': 'large', + 'audience': 'For larger teams', + 'bus_features': False, + 'deprecated': True, + }, + + # Active plans + { + 'title': 'Open Source', + 'price': 0, + 'privateRepos': 0, + 'stripeId': 'free', + 'audience': 'Committment to FOSS', + 'bus_features': False, + 'deprecated': False, + }, + { + 'title': 'Personal', + 'price': 1200, + 'privateRepos': 5, + 'stripeId': 'personal', + 'audience': 'Individuals', + 'bus_features': False, + 'deprecated': False, + }, + { + 'title': 'Skiff', + 'price': 2500, + 'privateRepos': 10, + 'stripeId': 'bus-micro', + 'audience': 'For startups', + 'bus_features': True, + 'deprecated': False, + }, + { + 'title': 'Yacht', + 'price': 5000, + 'privateRepos': 20, + 'stripeId': 'bus-small', + 'audience': 'For small businesses', + 'bus_features': True, + 'deprecated': False, + }, + { + 'title': 'Freighter', + 'price': 10000, + 'privateRepos': 50, + 'stripeId': 'bus-medium', + 'audience': 'For normal businesses', + 'bus_features': True, + 'deprecated': False, + }, + { + 'title': 'Tanker', + 'price': 20000, + 'privateRepos': 125, + 'stripeId': 'bus-large', + 'audience': 'For large businesses', + 'bus_features': True, + 'deprecated': False, + }, +] + + +def get_plan(plan_id): + """ Returns the plan with the given ID or None if none. """ + for plan in PLANS: + if plan['stripeId'] == plan_id: + return plan + + return None + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + @classmethod + def deep_copy(cls, attr_dict): + copy = AttrDict(attr_dict) + for key, value in copy.items(): + if isinstance(value, AttrDict): + copy[key] = cls.deep_copy(value) + return copy + + +class FakeStripe(object): + class Customer(AttrDict): + FAKE_PLAN = AttrDict({ + 'id': 'bus-small', + }) + + FAKE_SUBSCRIPTION = AttrDict({ + 'plan': FAKE_PLAN, + 'current_period_start': timegm(datetime.now().utctimetuple()), + 'current_period_end': timegm((datetime.now() + timedelta(days=30)).utctimetuple()), + 'trial_start': timegm(datetime.now().utctimetuple()), + 'trial_end': timegm((datetime.now() + timedelta(days=30)).utctimetuple()), + }) + + FAKE_CARD = AttrDict({ + 'id': 'card123', + 'name': 'Joe User', + 'type': 'Visa', + 'last4': '4242', + }) + + FAKE_CARD_LIST = AttrDict({ + 'data': [FAKE_CARD], + }) + + ACTIVE_CUSTOMERS = {} + + @property + def card(self): + return self.get('new_card', None) + + @card.setter + def card(self, card_token): + self['new_card'] = card_token + + @property + def plan(self): + return self.get('new_plan', None) + + @plan.setter + def plan(self, plan_name): + self['new_plan'] = plan_name + + def save(self): + if self.get('new_card', None) is not None: + raise stripe.CardError('Test raising exception on set card.', self.get('new_card'), 402) + if self.get('new_plan', None) is not None: + if self.subscription is None: + self.subscription = AttrDict.deep_copy(self.FAKE_SUBSCRIPTION) + self.subscription.plan.id = self.get('new_plan') + if self.get('cancel_subscription', None) is not None: + self.subscription = None + + def cancel_subscription(self): + self['cancel_subscription'] = True + + @classmethod + def retrieve(cls, stripe_customer_id): + if stripe_customer_id in cls.ACTIVE_CUSTOMERS: + cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('new_card', None) + cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('new_plan', None) + cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('cancel_subscription', None) + return cls.ACTIVE_CUSTOMERS[stripe_customer_id] + else: + new_customer = cls({ + 'default_card': 'card123', + 'cards': AttrDict.deep_copy(cls.FAKE_CARD_LIST), + 'subscription': AttrDict.deep_copy(cls.FAKE_SUBSCRIPTION), + 'id': stripe_customer_id, + }) + cls.ACTIVE_CUSTOMERS[stripe_customer_id] = new_customer + return new_customer + + class Invoice(AttrDict): + @staticmethod + def all(customer, count): + return AttrDict({ + 'data': [], + }) + + +class Billing(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.state = self.init_app(app) + else: + self.state = None + + def init_app(self, app): + billing_type = app.config.get('BILLING_TYPE', 'FakeStripe') + + if billing_type == 'Stripe': + billing = stripe + stripe.api_key = app.config.get('STRIPE_SECRET_KEY', None) + + elif billing_type == 'FakeStripe': + billing = FakeStripe + + else: + raise RuntimeError('Unknown billing type: %s' % billing_type) + + # register extension with app + app.extensions = getattr(app, 'extensions', {}) + app.extensions['billing'] = billing + return billing + + def __getattr__(self, name): + return getattr(self.state, name, None) diff --git a/data/database.py b/data/database.py index d6a67bd80..eaf2f0ff0 100644 --- a/data/database.py +++ b/data/database.py @@ -5,13 +5,39 @@ import uuid from random import SystemRandom from datetime import datetime from peewee import * +from sqlalchemy.engine.url import make_url +from urlparse import urlparse from app import app logger = logging.getLogger(__name__) -db = app.config['DB_DRIVER'](app.config['DB_NAME'], - **app.config['DB_CONNECTION_ARGS']) + + +SCHEME_DRIVERS = { + 'mysql': MySQLDatabase, + 'sqlite': SqliteDatabase, +} + + +def generate_db(config_object): + db_kwargs = dict(config_object['DB_CONNECTION_ARGS']) + parsed_url = make_url(config_object['DB_URI']) + + if parsed_url.host: + db_kwargs['host'] = parsed_url.host + if parsed_url.port: + db_kwargs['port'] = parsed_url.port + if parsed_url.username: + db_kwargs['user'] = parsed_url.username + if parsed_url.password: + db_kwargs['passwd'] = parsed_url.password + + return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs) + + +db = generate_db(app.config) + def random_string_generator(length=16): def random_string(): @@ -194,7 +220,8 @@ class ImageStorage(BaseModel): created = DateTimeField(null=True) comment = TextField(null=True) command = TextField(null=True) - image_size = BigIntegerField(null=True) + image_size = BigIntegerField(null=True) + uploading = BooleanField(default=True, null=True) class Image(BaseModel): @@ -249,7 +276,7 @@ class RepositoryBuild(BaseModel): class QueueItem(BaseModel): - queue_name = CharField(index=True) + queue_name = CharField(index=True, max_length=1024) body = TextField() available_after = DateTimeField(default=datetime.now, index=True) available = BooleanField(default=True, index=True) diff --git a/data/migrations/env.py b/data/migrations/env.py new file mode 100644 index 000000000..65f00819f --- /dev/null +++ b/data/migrations/env.py @@ -0,0 +1,76 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +from data.database import all_models +from app import app +from data.model.sqlalchemybridge import gen_sqlalchemy_metadata + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config +config.set_main_option('sqlalchemy.url', app.config['DB_URI']) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = gen_sqlalchemy_metadata(all_models) + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = app.config['DB_CONNECTION'] + context.configure(url=url, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + engine = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure( + connection=connection, + target_metadata=target_metadata + ) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() + diff --git a/data/migrations/script.py.mako b/data/migrations/script.py.mako new file mode 100644 index 000000000..95702017e --- /dev/null +++ b/data/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/data/model/legacy.py b/data/model/legacy.py index b6376d7d2..1c207cb79 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -4,14 +4,14 @@ import datetime import dateutil.parser import json - from data.database import * from util.validation import * from util.names import format_robot_username +from app import storage as store + logger = logging.getLogger(__name__) -store = app.config['STORAGE'] transaction_factory = app.config['DB_TRANSACTION_FACTORY'] class DataModelException(Exception): @@ -817,7 +817,7 @@ def get_repository(namespace_name, repository_name): def get_repo_image(namespace_name, repository_name, image_id): query = (Image - .select() + .select(Image, ImageStorage) .join(Repository) .switch(Image) .join(ImageStorage, JOIN_LEFT_OUTER) @@ -1489,7 +1489,8 @@ def get_pull_credentials(robotname): return { 'username': robot.username, 'password': login_info.service_ident, - 'registry': '%s://%s/v1/' % (app.config['URL_SCHEME'], app.config['URL_HOST']), + 'registry': '%s://%s/v1/' % (app.config['PREFERRED_URL_SCHEME'], + app.config['SERVER_HOSTNAME']), } @@ -1521,8 +1522,7 @@ def delete_webhook(namespace_name, repository_name, public_id): return webhook -def list_logs(user_or_organization_name, start_time, end_time, performer=None, - repository=None): +def list_logs(start_time, end_time, performer=None, repository=None, namespace=None): joined = LogEntry.select().join(User) if repository: joined = joined.where(LogEntry.repository == repository) @@ -1530,8 +1530,10 @@ def list_logs(user_or_organization_name, start_time, end_time, performer=None, if performer: joined = joined.where(LogEntry.performer == performer) + if namespace: + joined = joined.where(User.username == namespace) + return joined.where( - User.username == user_or_organization_name, LogEntry.datetime >= start_time, LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()) @@ -1633,3 +1635,15 @@ def delete_notifications_by_kind(target, kind): kind_ref = NotificationKind.get(name=kind) Notification.delete().where(Notification.target == target, Notification.kind == kind_ref).execute() + + +def get_active_users(): + return User.select().where(User.organization == False, User.robot == False) + +def get_active_user_count(): + return get_active_users().count() + +def delete_user(user): + user.delete_instance(recursive=True, delete_nullable=True) + + # TODO: also delete any repository data associated diff --git a/data/model/sqlalchemybridge.py b/data/model/sqlalchemybridge.py new file mode 100644 index 000000000..46809fb21 --- /dev/null +++ b/data/model/sqlalchemybridge.py @@ -0,0 +1,76 @@ +from sqlalchemy import (Table, MetaData, Column, ForeignKey, Integer, String, Boolean, Text, + DateTime, BigInteger, Index) +from peewee import (PrimaryKeyField, CharField, BooleanField, DateTimeField, TextField, + ForeignKeyField, BigIntegerField, IntegerField) + + +OPTIONS_TO_COPY = [ + 'null', + 'default', + 'primary_key', +] + + +OPTION_TRANSLATIONS = { + 'null': 'nullable', +} + + +def gen_sqlalchemy_metadata(peewee_model_list): + metadata = MetaData() + + for model in peewee_model_list: + meta = model._meta + + all_indexes = set(meta.indexes) + + columns = [] + for field in meta.get_fields(): + alchemy_type = None + col_args = [] + col_kwargs = {} + if isinstance(field, PrimaryKeyField): + alchemy_type = Integer + elif isinstance(field, CharField): + alchemy_type = String(field.max_length) + elif isinstance(field, BooleanField): + alchemy_type = Boolean + elif isinstance(field, DateTimeField): + alchemy_type = DateTime + elif isinstance(field, TextField): + alchemy_type = Text + elif isinstance(field, ForeignKeyField): + alchemy_type = Integer + target_name = '%s.%s' % (field.to_field.model_class._meta.db_table, + field.to_field.db_column) + col_args.append(ForeignKey(target_name)) + all_indexes.add(((field.name, ), field.unique)) + elif isinstance(field, BigIntegerField): + alchemy_type = BigInteger + elif isinstance(field, IntegerField): + alchemy_type = Integer + else: + raise RuntimeError('Unknown column type: %s' % field) + + for option_name in OPTIONS_TO_COPY: + alchemy_option_name = (OPTION_TRANSLATIONS[option_name] + if option_name in OPTION_TRANSLATIONS else option_name) + if alchemy_option_name not in col_kwargs: + option_val = getattr(field, option_name) + col_kwargs[alchemy_option_name] = option_val + + if field.unique or field.index: + all_indexes.add(((field.name, ), field.unique)) + + new_col = Column(field.db_column, alchemy_type, *col_args, **col_kwargs) + columns.append(new_col) + + new_table = Table(meta.db_table, metadata, *columns) + + for col_prop_names, unique in all_indexes: + col_names = [meta.fields[prop_name].db_column for prop_name in col_prop_names] + index_name = '%s_%s' % (meta.db_table, '_'.join(col_names)) + col_refs = [getattr(new_table.c, col_name) for col_name in col_names] + Index(index_name, *col_refs, unique=unique) + + return metadata diff --git a/data/plans.py b/data/plans.py deleted file mode 100644 index 2b8b6af2b..000000000 --- a/data/plans.py +++ /dev/null @@ -1,104 +0,0 @@ -PLANS = [ - # Deprecated Plans - { - 'title': 'Micro', - 'price': 700, - 'privateRepos': 5, - 'stripeId': 'micro', - 'audience': 'For smaller teams', - 'bus_features': False, - 'deprecated': True, - }, - { - 'title': 'Basic', - 'price': 1200, - 'privateRepos': 10, - 'stripeId': 'small', - 'audience': 'For your basic team', - 'bus_features': False, - 'deprecated': True, - }, - { - 'title': 'Medium', - 'price': 2200, - 'privateRepos': 20, - 'stripeId': 'medium', - 'audience': 'For medium teams', - 'bus_features': False, - 'deprecated': True, - }, - { - 'title': 'Large', - 'price': 5000, - 'privateRepos': 50, - 'stripeId': 'large', - 'audience': 'For larger teams', - 'bus_features': False, - 'deprecated': True, - }, - - # Active plans - { - 'title': 'Open Source', - 'price': 0, - 'privateRepos': 0, - 'stripeId': 'free', - 'audience': 'Committment to FOSS', - 'bus_features': False, - 'deprecated': False, - }, - { - 'title': 'Personal', - 'price': 1200, - 'privateRepos': 5, - 'stripeId': 'personal', - 'audience': 'Individuals', - 'bus_features': False, - 'deprecated': False, - }, - { - 'title': 'Skiff', - 'price': 2500, - 'privateRepos': 10, - 'stripeId': 'bus-micro', - 'audience': 'For startups', - 'bus_features': True, - 'deprecated': False, - }, - { - 'title': 'Yacht', - 'price': 5000, - 'privateRepos': 20, - 'stripeId': 'bus-small', - 'audience': 'For small businesses', - 'bus_features': True, - 'deprecated': False, - }, - { - 'title': 'Freighter', - 'price': 10000, - 'privateRepos': 50, - 'stripeId': 'bus-medium', - 'audience': 'For normal businesses', - 'bus_features': True, - 'deprecated': False, - }, - { - 'title': 'Tanker', - 'price': 20000, - 'privateRepos': 125, - 'stripeId': 'bus-large', - 'audience': 'For large businesses', - 'bus_features': True, - 'deprecated': False, - }, -] - - -def get_plan(plan_id): - """ Returns the plan with the given ID or None if none. """ - for plan in PLANS: - if plan['stripeId'] == plan_id: - return plan - - return None diff --git a/data/queue.py b/data/queue.py index 09e90f1a1..af1df3045 100644 --- a/data/queue.py +++ b/data/queue.py @@ -8,17 +8,26 @@ transaction_factory = app.config['DB_TRANSACTION_FACTORY'] class WorkQueue(object): - def __init__(self, queue_name): + def __init__(self, queue_name, canonical_name_match_list=None): self.queue_name = queue_name - def put(self, message, available_after=0, retries_remaining=5): + if canonical_name_match_list is None: + self.canonical_name_match_list = [] + else: + self.canonical_name_match_list = canonical_name_match_list + + @staticmethod + def _canonical_name(name_list): + return '/'.join(name_list) + '/' + + def put(self, canonical_name_list, message, available_after=0, retries_remaining=5): """ Put an item, if it shouldn't be processed for some number of seconds, specify that amount as available_after. """ params = { - 'queue_name': self.queue_name, + 'queue_name': self._canonical_name([self.queue_name] + canonical_name_list), 'body': message, 'retries_remaining': retries_remaining, } @@ -35,16 +44,25 @@ class WorkQueue(object): minutes. """ now = datetime.now() - available_or_expired = ((QueueItem.available == True) | - (QueueItem.processing_expires <= now)) + + name_match_query = '%s%%' % self._canonical_name([self.queue_name] + + self.canonical_name_match_list) with transaction_factory(db): - avail = QueueItem.select().where(QueueItem.queue_name == self.queue_name, - QueueItem.available_after <= now, - available_or_expired, - QueueItem.retries_remaining > 0) + running = (QueueItem + .select(QueueItem.queue_name) + .where(QueueItem.available == False, + QueueItem.processing_expires > now, + QueueItem.queue_name ** name_match_query)) - found = list(avail.limit(1).order_by(QueueItem.available_after)) + avail = QueueItem.select().where(QueueItem.queue_name ** name_match_query, + QueueItem.available_after <= now, + ((QueueItem.available == True) | + (QueueItem.processing_expires <= now)), + QueueItem.retries_remaining > 0, + ~(QueueItem.queue_name << running)) + + found = list(avail.limit(1).order_by(QueueItem.id)) if found: item = found[0] @@ -57,16 +75,24 @@ class WorkQueue(object): return None - def complete(self, completed_item): + @staticmethod + def complete(completed_item): completed_item.delete_instance() - def incomplete(self, incomplete_item, retry_after=300): + @staticmethod + def incomplete(incomplete_item, retry_after=300): retry_date = datetime.now() + timedelta(seconds=retry_after) incomplete_item.available_after = retry_date incomplete_item.available = True incomplete_item.save() + @staticmethod + def extend_processing(queue_item, seconds_from_now): + new_expiration = datetime.now() + timedelta(seconds=seconds_from_now) + queue_item.processing_expires = new_expiration + queue_item.save() -image_diff_queue = WorkQueue('imagediff') -dockerfile_build_queue = WorkQueue('dockerfilebuild3') -webhook_queue = WorkQueue('webhook') + +image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME']) +dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME']) +webhook_queue = WorkQueue(app.config['WEBHOOK_QUEUE_NAME']) diff --git a/data/userfiles.py b/data/userfiles.py index c01e23568..79fbcb507 100644 --- a/data/userfiles.py +++ b/data/userfiles.py @@ -1,25 +1,43 @@ import boto import os import logging +import hashlib +import magic from boto.s3.key import Key from uuid import uuid4 +from flask import url_for, request, send_file, make_response, abort +from flask.views import View logger = logging.getLogger(__name__) +class FakeUserfiles(object): + def prepare_for_drop(self, mime_type): + return ('http://fake/url', uuid4()) + + def store_file(self, file_like_obj, content_type): + raise NotImplementedError() + + def get_file_url(self, file_id, expires_in=300): + return ('http://fake/url') + + def get_file_checksum(self, file_id): + return 'abcdefg' + + class S3FileWriteException(Exception): pass -class UserRequestFiles(object): - def __init__(self, s3_access_key, s3_secret_key, bucket_name): +class S3Userfiles(object): + def __init__(self, path, s3_access_key, s3_secret_key, bucket_name): self._initialized = False self._bucket_name = bucket_name self._access_key = s3_access_key self._secret_key = s3_secret_key - self._prefix = 'userfiles' + self._prefix = path self._s3_conn = None self._bucket = None @@ -70,3 +88,139 @@ class UserRequestFiles(object): full_key = os.path.join(self._prefix, file_id) k = self._bucket.lookup(full_key) return k.etag[1:-1][:7] + + +class UserfilesHandlers(View): + methods = ['GET', 'PUT'] + + def __init__(self, local_userfiles): + self._userfiles = local_userfiles + self._magic = magic.Magic(mime=True) + + def get(self, file_id): + path = self._userfiles.file_path(file_id) + if not os.path.exists(path): + abort(404) + + logger.debug('Sending path: %s' % path) + return send_file(path, mimetype=self._magic.from_file(path)) + + def put(self, file_id): + input_stream = request.stream + if request.headers.get('transfer-encoding') == 'chunked': + # Careful, might work only with WSGI servers supporting chunked + # encoding (Gunicorn) + input_stream = request.environ['wsgi.input'] + + self._userfiles.store_stream(input_stream, file_id) + + return make_response('Okay') + + def dispatch_request(self, file_id): + if request.method == 'GET': + return self.get(file_id) + elif request.method == 'PUT': + return self.put(file_id) + + +class LocalUserfiles(object): + def __init__(self, app, path): + self._root_path = path + self._buffer_size = 64 * 1024 # 64 KB + self._app = app + + def _build_url_adapter(self): + return self._app.url_map.bind(self._app.config['SERVER_HOSTNAME'], + script_name=self._app.config['APPLICATION_ROOT'] or '/', + url_scheme=self._app.config['PREFERRED_URL_SCHEME']) + + def prepare_for_drop(self, mime_type): + file_id = str(uuid4()) + with self._app.app_context() as ctx: + ctx.url_adapter = self._build_url_adapter() + return (url_for('userfiles_handlers', file_id=file_id, _external=True), file_id) + + def file_path(self, file_id): + if '..' in file_id or file_id.startswith('/'): + raise RuntimeError('Invalid Filename') + return os.path.join(self._root_path, file_id) + + def store_stream(self, stream, file_id): + path = self.file_path(file_id) + dirname = os.path.dirname(path) + if not os.path.exists(dirname): + os.makedirs(dirname) + + with open(path, 'w') as to_write: + while True: + try: + buf = stream.read(self._buffer_size) + if not buf: + break + to_write.write(buf) + except IOError: + break + + def store_file(self, file_like_obj, content_type): + file_id = str(uuid4()) + + # Rewind the file to match what s3 does + file_like_obj.seek(0, os.SEEK_SET) + + self.store_stream(file_like_obj, file_id) + return file_id + + def get_file_url(self, file_id, expires_in=300): + with self._app.app_context() as ctx: + ctx.url_adapter = self._build_url_adapter() + return url_for('userfiles_handlers', file_id=file_id, _external=True) + + def get_file_checksum(self, file_id): + path = self.file_path(file_id) + sha_hash = hashlib.sha256() + with open(path, 'r') as to_hash: + while True: + buf = to_hash.read(self._buffer_size) + if not buf: + break + sha_hash.update(buf) + return sha_hash.hexdigest()[:7] + + +class Userfiles(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.state = self.init_app(app) + else: + self.state = None + + def init_app(self, app): + storage_type = app.config.get('USERFILES_TYPE', 'LocalUserfiles') + path = app.config.get('USERFILES_PATH', '') + + if storage_type == 'LocalUserfiles': + userfiles = LocalUserfiles(app, path) + app.add_url_rule('/userfiles/', + view_func=UserfilesHandlers.as_view('userfiles_handlers', + local_userfiles=userfiles)) + + elif storage_type == 'S3Userfiles': + access_key = app.config.get('USERFILES_AWS_ACCESS_KEY', '') + secret_key = app.config.get('USERFILES_AWS_SECRET_KEY', '') + bucket = app.config.get('USERFILES_S3_BUCKET', '') + userfiles = S3Userfiles(path, access_key, secret_key, bucket) + + elif storage_type == 'FakeUserfiles': + userfiles = FakeUserfiles() + + else: + raise RuntimeError('Unknown userfiles type: %s' % storage_type) + + # register extension with app + app.extensions = getattr(app, 'extensions', {}) + app.extensions['userfiles'] = userfiles + return userfiles + + def __getattr__(self, name): + return getattr(self.state, name, None) diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 60b5d7398..3abefa54f 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -85,11 +85,32 @@ def handle_api_error(error): def resource(*urls, **kwargs): def wrapper(api_resource): + if not api_resource: + return None + api.add_resource(api_resource, *urls, **kwargs) return api_resource return wrapper +def show_if(value): + def f(inner): + if not value: + return None + + return inner + return f + + +def hide_if(value): + def f(inner): + if value: + return None + + return inner + return f + + def truthy_bool(param): return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'} @@ -103,6 +124,9 @@ def format_date(date): def add_method_metadata(name, value): def modifier(func): + if func is None: + return None + if '__api_metadata' not in dir(func): func.__api_metadata = {} func.__api_metadata[name] = value @@ -111,11 +135,15 @@ def add_method_metadata(name, value): def method_metadata(func, name): + if func is None: + return None + if '__api_metadata' in dir(func): return func.__api_metadata.get(name, None) return None + nickname = partial(add_method_metadata, 'nickname') related_user_resource = partial(add_method_metadata, 'related_user_resource') internal_only = add_method_metadata('internal', True) @@ -274,6 +302,7 @@ import endpoints.api.repository import endpoints.api.repotoken import endpoints.api.robot import endpoints.api.search +import endpoints.api.superuser import endpoints.api.tag import endpoints.api.team import endpoints.api.trigger diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 1f31aa58b..6880308a0 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -1,16 +1,17 @@ import stripe from flask import request - +from app import billing from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin) + require_user_admin, show_if, hide_if) from endpoints.api.subscribe import subscribe, subscription_view from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model -from data.plans import PLANS +from data.billing import PLANS +import features def carderror_response(e): return {'carderror': e.message}, 402 @@ -22,7 +23,7 @@ def get_card(user): } if user.stripe_id: - cus = stripe.Customer.retrieve(user.stripe_id) + cus = billing.Customer.retrieve(user.stripe_id) if cus and cus.default_card: # Find the default card. default_card = None @@ -43,7 +44,7 @@ def get_card(user): def set_card(user, token): if user.stripe_id: - cus = stripe.Customer.retrieve(user.stripe_id) + cus = billing.Customer.retrieve(user.stripe_id) if cus: try: cus.card = token @@ -72,13 +73,14 @@ def get_invoices(customer_id): 'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None } - invoices = stripe.Invoice.all(customer=customer_id, count=12) + invoices = billing.Invoice.all(customer=customer_id, count=12) return { 'invoices': [invoice_view(i) for i in invoices.data] } @resource('/v1/plans/') +@show_if(features.BILLING) class ListPlans(ApiResource): """ Resource for listing the available plans. """ @nickname('listPlans') @@ -91,6 +93,7 @@ class ListPlans(ApiResource): @resource('/v1/user/card') @internal_only +@show_if(features.BILLING) class UserCard(ApiResource): """ Resource for managing a user's credit card. """ schemas = { @@ -132,6 +135,7 @@ class UserCard(ApiResource): @resource('/v1/organization//card') @internal_only @related_user_resource(UserCard) +@show_if(features.BILLING) class OrganizationCard(ApiResource): """ Resource for managing an organization's credit card. """ schemas = { @@ -178,6 +182,7 @@ class OrganizationCard(ApiResource): @resource('/v1/user/plan') @internal_only +@show_if(features.BILLING) class UserPlan(ApiResource): """ Resource for managing a user's subscription. """ schemas = { @@ -216,16 +221,19 @@ class UserPlan(ApiResource): @nickname('getUserSubscription') def get(self): """ Fetch any existing subscription for the user. """ + cus = None user = get_authenticated_user() private_repos = model.get_private_repo_count(user.username) if user.stripe_id: - cus = stripe.Customer.retrieve(user.stripe_id) + cus = billing.Customer.retrieve(user.stripe_id) if cus.subscription: return subscription_view(cus.subscription, private_repos) return { + 'hasSubscription': False, + 'isExistingCustomer': cus is not None, 'plan': 'free', 'usedPrivateRepos': private_repos, } @@ -234,6 +242,7 @@ class UserPlan(ApiResource): @resource('/v1/organization//plan') @internal_only @related_user_resource(UserPlan) +@show_if(features.BILLING) class OrganizationPlan(ApiResource): """ Resource for managing a org's subscription. """ schemas = { @@ -274,17 +283,20 @@ class OrganizationPlan(ApiResource): @nickname('getOrgSubscription') def get(self, orgname): """ Fetch any existing subscription for the org. """ + cus = None permission = AdministerOrganizationPermission(orgname) if permission.can(): private_repos = model.get_private_repo_count(orgname) organization = model.get_organization(orgname) if organization.stripe_id: - cus = stripe.Customer.retrieve(organization.stripe_id) + cus = billing.Customer.retrieve(organization.stripe_id) if cus.subscription: return subscription_view(cus.subscription, private_repos) return { + 'hasSubscription': False, + 'isExistingCustomer': cus is not None, 'plan': 'free', 'usedPrivateRepos': private_repos, } @@ -294,6 +306,7 @@ class OrganizationPlan(ApiResource): @resource('/v1/user/invoices') @internal_only +@show_if(features.BILLING) class UserInvoiceList(ApiResource): """ Resource for listing a user's invoices. """ @require_user_admin @@ -310,6 +323,7 @@ class UserInvoiceList(ApiResource): @resource('/v1/organization//invoices') @internal_only @related_user_resource(UserInvoiceList) +@show_if(features.BILLING) class OrgnaizationInvoiceList(ApiResource): """ Resource for listing an orgnaization's invoices. """ @nickname('listOrgInvoices') @@ -323,4 +337,4 @@ class OrgnaizationInvoiceList(ApiResource): return get_invoices(organization.stripe_id) - raise Unauthorized() \ No newline at end of file + raise Unauthorized() diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 9fa130054..c9bd2cf3a 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -3,7 +3,7 @@ import json from flask import request -from app import app +from app import app, userfiles as user_files from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, require_repo_read, require_repo_write, validate_json_request, ApiResource, internal_only, format_date, api, Unauthorized, NotFound) @@ -17,7 +17,6 @@ from util.names import parse_robot_username logger = logging.getLogger(__name__) -user_files = app.config['USERFILES'] build_logs = app.config['BUILDLOGS'] diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 212d6654e..6e7eb3f5a 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -23,13 +23,12 @@ TYPE_CONVERTER = { int: 'integer', } -URL_SCHEME = app.config['URL_SCHEME'] -URL_HOST = app.config['URL_HOST'] +PREFERRED_URL_SCHEME = app.config['PREFERRED_URL_SCHEME'] +SERVER_HOSTNAME = app.config['SERVER_HOSTNAME'] def fully_qualified_name(method_view_class): - inst = method_view_class() - return '%s.%s' % (inst.__module__, inst.__class__.__name__) + return '%s.%s' % (method_view_class.__module__, method_view_class.__name__) def swagger_route_data(include_internal=False, compact=False): @@ -143,7 +142,7 @@ def swagger_route_data(include_internal=False, compact=False): swagger_data = { 'apiVersion': 'v1', 'swaggerVersion': '1.2', - 'basePath': '%s://%s' % (URL_SCHEME, URL_HOST), + 'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), 'resourcePath': '/', 'info': { 'title': 'Quay.io API', @@ -160,7 +159,7 @@ def swagger_route_data(include_internal=False, compact=False): "implicit": { "tokenName": "access_token", "loginEndpoint": { - "url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST), + "url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), }, }, }, diff --git a/endpoints/api/image.py b/endpoints/api/image.py index edfae14b8..4571b140b 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -2,16 +2,13 @@ import json from collections import defaultdict -from app import app +from app import storage as store from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource, format_date, NotFound) from data import model from util.cache import cache_control_flask_restful -store = app.config['STORAGE'] - - def image_view(image): extended_props = image if image.storage and image.storage.id: diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index abd2c3e03..2ce2bbb30 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -29,8 +29,7 @@ def log_view(log): return view -def get_logs(namespace, start_time, end_time, performer_name=None, - repository=None): +def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None): performer = None if performer_name: performer = model.get_user(performer_name) @@ -54,8 +53,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None, if not end_time: end_time = datetime.today() - logs = model.list_logs(namespace, start_time, end_time, performer=performer, - repository=repository) + logs = model.list_logs(start_time, end_time, performer=performer, repository=repository, + namespace=namespace) return { 'start_time': format_date(start_time), 'end_time': format_date(end_time), @@ -80,7 +79,7 @@ class RepositoryLogs(RepositoryParamResource): start_time = args['starttime'] end_time = args['endtime'] - return get_logs(namespace, start_time, end_time, repository=repo) + return get_logs(start_time, end_time, repository=repo, namespace=namespace) @resource('/v1/user/logs') @@ -100,7 +99,7 @@ class UserLogs(ApiResource): end_time = args['endtime'] user = get_authenticated_user() - return get_logs(user.username, start_time, end_time, performer_name=performer_name) + return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username) @resource('/v1/organization//logs') @@ -121,6 +120,6 @@ class OrgLogs(ApiResource): start_time = args['starttime'] end_time = args['endtime'] - return get_logs(orgname, start_time, end_time, performer_name=performer_name) + return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name) - raise Unauthorized() \ No newline at end of file + raise Unauthorized() diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 9cb6a267a..f6a381ace 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -1,20 +1,22 @@ import logging -import stripe from flask import request +from app import billing as stripe from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, log_action) + require_user_admin, log_action, show_if) from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, CreateRepositoryPermission) from auth.auth_context import get_authenticated_user from data import model -from data.plans import get_plan +from data.billing import get_plan from util.gravatar import compute_hash +import features + logger = logging.getLogger(__name__) @@ -163,6 +165,7 @@ class Organization(ApiResource): @resource('/v1/organization//private') @internal_only @related_user_resource(PrivateRepositories) +@show_if(features.BILLING) class OrgPrivateRepositories(ApiResource): """ Custom verb to compute whether additional private repositories are available. """ @nickname('getOrganizationPrivateAllowed') diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py index f9f9d7f14..dd6de9678 100644 --- a/endpoints/api/subscribe.py +++ b/endpoints/api/subscribe.py @@ -1,11 +1,13 @@ import logging import stripe +from app import billing from endpoints.api import request_error, log_action, NotFound from endpoints.common import check_repository_usage from data import model -from data.plans import PLANS +from data.billing import PLANS +import features logger = logging.getLogger(__name__) @@ -15,15 +17,24 @@ def carderror_response(exc): def subscription_view(stripe_subscription, used_repos): - return { + view = { + 'hasSubscription': True, + 'isExistingCustomer': True, 'currentPeriodStart': stripe_subscription.current_period_start, 'currentPeriodEnd': stripe_subscription.current_period_end, 'plan': stripe_subscription.plan.id, 'usedPrivateRepos': used_repos, + 'trialStart': stripe_subscription.trial_start, + 'trialEnd': stripe_subscription.trial_end } + return view + def subscribe(user, plan, token, require_business_plan): + if not features.BILLING: + return + plan_found = None for plan_obj in PLANS: if plan_obj['stripeId'] == plan: @@ -56,7 +67,7 @@ def subscribe(user, plan, token, require_business_plan): card = token try: - cus = stripe.Customer.create(email=user.email, plan=plan, card=card) + cus = billing.Customer.create(email=user.email, plan=plan, card=card) user.stripe_id = cus.id user.save() check_repository_usage(user, plan_found) @@ -69,7 +80,7 @@ def subscribe(user, plan, token, require_business_plan): else: # Change the plan - cus = stripe.Customer.retrieve(user.stripe_id) + cus = billing.Customer.retrieve(user.stripe_id) if plan_found['price'] == 0: if cus.subscription is not None: diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py new file mode 100644 index 000000000..2688f6945 --- /dev/null +++ b/endpoints/api/superuser.py @@ -0,0 +1,160 @@ +import logging +import json + +from app import app + +from flask import request + +from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, + log_action, internal_only, NotFound, require_user_admin, format_date, + InvalidToken, require_scope, format_date, hide_if, show_if, parse_args, + query_param, abort) + +from endpoints.api.logs import get_logs + +from data import model +from auth.permissions import SuperUserPermission +from auth.auth_context import get_authenticated_user + +import features + +logger = logging.getLogger(__name__) + +@resource('/v1/superuser/logs') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserLogs(ApiResource): + """ Resource for fetching all logs in the system. """ + @nickname('listAllLogs') + @parse_args + @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('performer', 'Username for which to filter logs.', type=str) + def get(self, args): + """ List the logs for the current system. """ + if SuperUserPermission().can(): + performer_name = args['performer'] + start_time = args['starttime'] + end_time = args['endtime'] + + return get_logs(start_time, end_time) + + abort(403) + + +@resource('/v1/superuser/seats') +@internal_only +@show_if(features.SUPER_USERS) +@hide_if(features.BILLING) +class SeatUsage(ApiResource): + """ Resource for managing the seats granted in the license for the system. """ + @nickname('getSeatCount') + def get(self): + """ Returns the current number of seats being used in the system. """ + if SuperUserPermission().can(): + return { + 'count': model.get_active_user_count(), + 'allowed': app.config.get('LICENSE_SEAT_COUNT', 0) + } + + abort(403) + + +def user_view(user): + return { + 'username': user.username, + 'email': user.email, + 'verified': user.verified, + 'super_user': user.username in app.config['SUPER_USERS'] + } + +@resource('/v1/superuser/users/') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserList(ApiResource): + """ Resource for listing users in the system. """ + @nickname('listAllUsers') + def get(self): + """ Returns a list of all users in the system. """ + if SuperUserPermission().can(): + users = model.get_active_users() + return { + 'users': [user_view(user) for user in users] + } + + abort(403) + + +@resource('/v1/superuser/users/') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserManagement(ApiResource): + """ Resource for managing users in the system. """ + schemas = { + 'UpdateUser': { + 'id': 'UpdateUser', + 'type': 'object', + 'description': 'Description of updates for a user', + 'properties': { + 'password': { + 'type': 'string', + 'description': 'The new password for the user', + }, + 'email': { + 'type': 'string', + 'description': 'The new e-mail address for the user', + } + }, + }, + } + + @nickname('getInstallUser') + def get(self, username): + """ Returns information about the specified user. """ + if SuperUserPermission().can(): + user = model.get_user(username) + if not user or user.organization or user.robot: + abort(404) + + return user_view(user) + + abort(403) + + @nickname('deleteInstallUser') + def delete(self, username): + """ Deletes the specified user. """ + if SuperUserPermission().can(): + user = model.get_user(username) + if not user or user.organization or user.robot: + abort(404) + + if username in app.config['SUPER_USERS']: + abort(403) + + model.delete_user(user) + return 'Deleted', 204 + + abort(403) + + @nickname('changeInstallUser') + @validate_json_request('UpdateUser') + def put(self, username): + """ Updates information about the specified user. """ + if SuperUserPermission().can(): + user = model.get_user(username) + if not user or user.organization or user.robot: + abort(404) + + if username in app.config['SUPER_USERS']: + abort(403) + + user_data = request.get_json() + if 'password' in user_data: + model.change_password(user, user_data['password']) + + if 'email' in user_data: + model.update_email(user, user_data['email']) + + return user_view(user) + + abort(403) diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index a5783e59b..10d466e81 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -1,5 +1,7 @@ -from endpoints.api import (resource, nickname, require_repo_read, require_repo_admin, - RepositoryParamResource, log_action, NotFound) +from flask import request + +from endpoints.api import (resource, nickname, require_repo_read, require_repo_write, + RepositoryParamResource, log_action, NotFound, validate_json_request) from endpoints.api.image import image_view from data import model from auth.auth_context import get_authenticated_user @@ -8,8 +10,54 @@ from auth.auth_context import get_authenticated_user @resource('/v1/repository//tag/') class RepositoryTag(RepositoryParamResource): """ Resource for managing repository tags. """ + schemas = { + 'MoveTag': { + 'id': 'MoveTag', + 'type': 'object', + 'description': 'Description of to which image a new or existing tag should point', + 'required': [ + 'image', + ], + 'properties': { + 'image': { + 'type': 'string', + 'description': 'Image identifier to which the tag should point', + }, + }, + }, + } - @require_repo_admin + @require_repo_write + @nickname('changeTagImage') + @validate_json_request('MoveTag') + def put(self, namespace, repository, tag): + """ Change which image a tag points to or create a new tag.""" + image_id = request.get_json()['image'] + image = model.get_repo_image(namespace, repository, image_id) + if not image: + raise NotFound() + + original_image_id = None + try: + original_tag_image = model.get_tag_image(namespace, repository, tag) + if original_tag_image: + original_image_id = original_tag_image.docker_image_id + except model.DataModelException: + # This is a new tag. + pass + + model.create_or_update_tag(namespace, repository, tag, image_id) + model.garbage_collect_repository(namespace, repository) + + username = get_authenticated_user().username + log_action('move_tag' if original_image_id else 'create_tag', namespace, + { 'username': username, 'repo': repository, 'tag': tag, + 'image': image_id, 'original_image': original_image_id }, + repo=model.get_repository(namespace, repository)) + + return 'Updated', 201 + + @require_repo_write @nickname('deleteFullTag') def delete(self, namespace, repository, tag): """ Delete the specified repository tag. """ diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 7798280a1..20ab04330 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -205,9 +205,8 @@ class BuildTriggerActivate(RepositoryParamResource): trigger.repository.name) path = url_for('webhooks.build_trigger_webhook', repository=repository_path, trigger_uuid=trigger.uuid) - authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token', - token.code, app.config['URL_HOST'], - path) + authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code, + app.config['SERVER_HOSTNAME'], path) final_config = handler.activate(trigger.uuid, authed_url, trigger.auth_token, new_config_dict) @@ -294,7 +293,7 @@ class BuildTriggerAnalyze(RepositoryParamResource): } # Check to see if the base image lives in Quay. - quay_registry_prefix = '%s/' % (app.config['URL_HOST']) + quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME']) if not base_image.startswith(quay_registry_prefix): return { diff --git a/endpoints/api/user.py b/endpoints/api/user.py index ba245209c..437f37450 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -1,27 +1,27 @@ import logging -import stripe import json from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity -from app import app +from app import app, billing as stripe from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, - InvalidToken, require_scope, format_date) + InvalidToken, require_scope, format_date, hide_if, show_if) from endpoints.api.subscribe import subscribe from endpoints.common import common_login from data import model -from data.plans import get_plan +from data.billing import get_plan from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission, - UserAdminPermission, UserReadPermission) + UserAdminPermission, UserReadPermission, SuperUserPermission) from auth.auth_context import get_authenticated_user from auth import scopes from util.gravatar import compute_hash from util.email import (send_confirmation_email, send_recovery_email, send_change_email) +import features logger = logging.getLogger(__name__) @@ -65,6 +65,11 @@ def user_view(user): 'preferred_namespace': not (user.stripe_id is None), }) + if features.SUPER_USERS: + user_response.update({ + 'super_user': user and user == get_authenticated_user() and SuperUserPermission().can() + }) + return user_response @@ -193,6 +198,7 @@ class User(ApiResource): @resource('/v1/user/private') @internal_only +@show_if(features.BILLING) class PrivateRepositories(ApiResource): """ Operations dealing with the available count of private repositories. """ @require_user_admin @@ -248,8 +254,7 @@ class ConvertToOrganization(ApiResource): 'description': 'Information required to convert a user to an organization.', 'required': [ 'adminUser', - 'adminPassword', - 'plan', + 'adminPassword' ], 'properties': { 'adminUser': { @@ -262,7 +267,7 @@ class ConvertToOrganization(ApiResource): }, 'plan': { 'type': 'string', - 'description': 'The plan to which the organizatino should be subscribed', + 'description': 'The plan to which the organization should be subscribed', }, }, }, @@ -289,8 +294,9 @@ class ConvertToOrganization(ApiResource): message='The admin user credentials are not valid') # Subscribe the organization to the new plan. - plan = convert_data['plan'] - subscribe(user, plan, None, True) # Require business plans + if features.BILLING: + plan = convert_data.get('plan', 'free') + subscribe(user, plan, None, True) # Require business plans # Convert the user to an organization. model.convert_user_to_organization(user, model.get_user(admin_username)) diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 0f110c098..43aa2ac6b 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -3,14 +3,15 @@ import logging from flask import request, redirect, url_for, Blueprint from flask.ext.login import current_user -from endpoints.common import render_page_template, common_login -from app import app, mixpanel +from endpoints.common import render_page_template, common_login, route_show_if +from app import app, analytics from data import model from util.names import parse_repository_name from util.http import abort from auth.permissions import AdministerRepositoryPermission from auth.auth import require_session_login +import features logger = logging.getLogger(__name__) @@ -20,11 +21,11 @@ client = app.config['HTTPCLIENT'] callback = Blueprint('callback', __name__) -def exchange_github_code_for_token(code): +def exchange_github_code_for_token(code, for_login=True): code = request.args.get('code') payload = { - 'client_id': app.config['GITHUB_CLIENT_ID'], - 'client_secret': app.config['GITHUB_CLIENT_SECRET'], + 'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'], + 'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'], 'code': code, } headers = { @@ -48,6 +49,7 @@ def get_github_user(token): @callback.route('/github/callback', methods=['GET']) +@route_show_if(features.GITHUB_LOGIN) def github_oauth_callback(): error = request.args.get('error', None) if error: @@ -83,13 +85,13 @@ def github_oauth_callback(): to_login = model.create_federated_user(username, found_email, 'github', github_id) - # Success, tell mixpanel - mixpanel.track(to_login.username, 'register', {'service': 'github'}) + # Success, tell analytics + analytics.track(to_login.username, 'register', {'service': 'github'}) state = request.args.get('state', None) if state: logger.debug('Aliasing with state: %s' % state) - mixpanel.alias(to_login.username, state) + analytics.alias(to_login.username, state) except model.DataModelException, ex: return render_page_template('githuberror.html', error_message=ex.message) @@ -101,6 +103,7 @@ def github_oauth_callback(): @callback.route('/github/callback/attach', methods=['GET']) +@route_show_if(features.GITHUB_LOGIN) @require_session_login def github_oauth_attach(): token = exchange_github_code_for_token(request.args.get('code')) @@ -117,7 +120,7 @@ def github_oauth_attach(): def attach_github_build_trigger(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - token = exchange_github_code_for_token(request.args.get('code')) + token = exchange_github_code_for_token(request.args.get('code'), for_login=False) repo = model.get_repository(namespace, repository) if not repo: msg = 'Invalid repository: %s/%s' % (namespace, repository) diff --git a/endpoints/common.py b/endpoints/common.py index 749cf3ba0..e9bd7b7c6 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -3,7 +3,7 @@ import urlparse import json import string -from flask import make_response, render_template, request +from flask import make_response, render_template, request, abort from flask.ext.login import login_user, UserMixin from flask.ext.principal import identity_changed from random import SystemRandom @@ -15,7 +15,10 @@ from auth.permissions import QuayDeferredPermissionUser from auth import scopes from endpoints.api.discovery import swagger_route_data from werkzeug.routing import BaseConverter +from functools import wraps +from config import getFrontendVisibleConfig +import features logger = logging.getLogger(__name__) @@ -27,6 +30,29 @@ class RepoPathConverter(BaseConverter): app.url_map.converters['repopath'] = RepoPathConverter +def route_show_if(value): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not value: + abort(404) + + return f(*args, **kwargs) + return decorated_function + return decorator + + +def route_hide_if(value): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if value: + abort(404) + + return f(*args, **kwargs) + return decorated_function + return decorator + def get_route_data(): global route_data @@ -89,9 +115,52 @@ def random_string(): random = SystemRandom() return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)]) +def list_files(path, extension): + import os + def matches(f): + return os.path.splitext(f)[1] == '.' + extension + + def join_path(dp, f): + # Remove the static/ prefix. It is added in the template. + return os.path.join(dp, f)[len('static/'):] + + filepath = 'static/' + path + return [join_path(dp, f) for dp, dn, files in os.walk(filepath) for f in files if matches(f)] + def render_page_template(name, **kwargs): + if app.config.get('DEBUGGING', False): + # If DEBUGGING is enabled, then we load the full set of individual JS and CSS files + # from the file system. + library_styles = list_files('lib', 'css') + main_styles = list_files('css', 'css') + library_scripts = list_files('lib', 'js') + main_scripts = list_files('js', 'js') + cache_buster = 'debugging' + + file_lists = [library_styles, main_styles, library_scripts, main_scripts] + for file_list in file_lists: + file_list.sort() + else: + library_styles = [] + main_styles = ['dist/quay-frontend.css'] + library_scripts = [] + main_scripts = ['dist/quay-frontend.min.js'] + cache_buster = random_string() + resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), - cache_buster=random_string(), **kwargs)) + main_styles=main_styles, + library_styles=library_styles, + main_scripts=main_scripts, + library_scripts=library_scripts, + feature_set=json.dumps(features.get_features()), + config_set=json.dumps(getFrontendVisibleConfig(app.config)), + mixpanel_key=app.config.get('MIXPANEL_KEY', ''), + sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), + is_debug=str(app.config.get('DEBUGGING', False)).lower(), + show_chat=features.OLARK_CHAT, + cache_buster=cache_buster, + **kwargs)) + resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp @@ -125,7 +194,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, dockerfile_id, build_name, trigger, pull_robot_name = pull_robot_name) - dockerfile_build_queue.put(json.dumps({ + dockerfile_build_queue.put([repository.namespace, repository.name], json.dumps({ 'build_uuid': build_request.uuid, 'namespace': repository.namespace, 'repository': repository.name, diff --git a/endpoints/index.py b/endpoints/index.py index 480c9e636..6ebec2d6c 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -9,7 +9,7 @@ from collections import OrderedDict from data import model from data.model import oauth from data.queue import webhook_queue -from app import mixpanel, app +from app import analytics, app from auth.auth import process_auth from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token from util.names import parse_repository_name @@ -21,6 +21,7 @@ from util.http import abort logger = logging.getLogger(__name__) +profile = logging.getLogger('application.profiler') index = Blueprint('index', __name__) @@ -112,9 +113,15 @@ def create_user(): else: # New user case + profile.debug('Creating user') new_user = model.create_user(username, password, user_data['email']) + + profile.debug('Creating email code for user') code = model.create_confirm_email_code(new_user) + + profile.debug('Sending email code to user') send_confirmation_email(new_user.username, new_user.email, code.code) + return make_response('Created', 201) @@ -149,12 +156,12 @@ def update_user(username): update_request = request.get_json() if 'password' in update_request: - logger.debug('Updating user password.') + profile.debug('Updating user password') model.change_password(get_authenticated_user(), update_request['password']) if 'email' in update_request: - logger.debug('Updating user email') + profile.debug('Updating user email') model.update_email(get_authenticated_user(), update_request['email']) return jsonify({ @@ -170,9 +177,13 @@ def update_user(username): @parse_repository_name @generate_headers(role='write') def create_repository(namespace, repository): + profile.debug('Parsing image descriptions') image_descriptions = json.loads(request.data) + + profile.debug('Looking up repository') repo = model.get_repository(namespace, repository) + profile.debug('Repository looked up') if not repo and get_authenticated_user() is None: logger.debug('Attempt to create new repository without user auth.') abort(401, @@ -196,11 +207,11 @@ def create_repository(namespace, repository): issue='no-create-permission', namespace=namespace) - logger.debug('Creaing repository with owner: %s' % - get_authenticated_user().username) + profile.debug('Creaing repository with owner: %s', get_authenticated_user().username) repo = model.create_repository(namespace, repository, get_authenticated_user()) + profile.debug('Determining added images') added_images = OrderedDict([(desc['id'], desc) for desc in image_descriptions]) new_repo_images = dict(added_images) @@ -209,12 +220,15 @@ def create_repository(namespace, repository): if existing.docker_image_id in new_repo_images: added_images.pop(existing.docker_image_id) + profile.debug('Creating/Linking necessary images') username = get_authenticated_user() and get_authenticated_user().username translations = {} for image_description in added_images.values(): model.find_create_or_link_image(image_description['id'], repo, username, translations) + + profile.debug('Created images') response = make_response('Created', 201) extra_params = { @@ -227,7 +241,7 @@ def create_repository(namespace, repository): } if get_validated_oauth_token(): - mixpanel.track(username, 'push_repo', extra_params) + analytics.track(username, 'push_repo', extra_params) oauth_token = get_validated_oauth_token() metadata['oauth_token_id'] = oauth_token.id @@ -236,7 +250,7 @@ def create_repository(namespace, repository): elif get_authenticated_user(): username = get_authenticated_user().username - mixpanel.track(username, 'push_repo', extra_params) + analytics.track(username, 'push_repo', extra_params) metadata['username'] = username # Mark that the user has started pushing the repo. @@ -250,7 +264,7 @@ def create_repository(namespace, repository): event.publish_event_data('docker-cli', user_data) elif get_validated_token(): - mixpanel.track(get_validated_token().code, 'push_repo', extra_params) + analytics.track(get_validated_token().code, 'push_repo', extra_params) metadata['token'] = get_validated_token().friendly_name metadata['token_code'] = get_validated_token().code @@ -268,21 +282,23 @@ def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): + profile.debug('Looking up repository') repo = model.get_repository(namespace, repository) if not repo: # Make sure the repo actually exists. abort(404, message='Unknown repository', issue='unknown-repo') + profile.debug('Parsing image data') image_with_checksums = json.loads(request.data) updated_tags = {} for image in image_with_checksums: - logger.debug('Setting checksum for image id: %s to %s' % - (image['id'], image['checksum'])) + profile.debug('Setting checksum for image id: %s to %s', image['id'], image['checksum']) updated_tags[image['Tag']] = image['id'] model.set_image_checksum(image['id'], repo, image['checksum']) if get_authenticated_user(): + profile.debug('Publishing push event') username = get_authenticated_user().username # Mark that the user has pushed the repo. @@ -295,15 +311,18 @@ def update_images(namespace, repository): event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', user_data) + profile.debug('GCing repository') num_removed = model.garbage_collect_repository(namespace, repository) # Generate a job for each webhook that has been added to this repo + profile.debug('Adding webhooks for repository') + webhooks = model.list_webhooks(namespace, repository) for webhook in webhooks: webhook_data = json.loads(webhook.parameters) repo_string = '%s/%s' % (namespace, repository) - logger.debug('Creating webhook for repository \'%s\' for url \'%s\'' % - (repo_string, webhook_data['url'])) + profile.debug('Creating webhook for repository \'%s\' for url \'%s\'', + repo_string, webhook_data['url']) webhook_data['payload'] = { 'repository': repo_string, 'namespace': namespace, @@ -315,7 +334,7 @@ def update_images(namespace, repository): 'pushed_image_count': len(image_with_checksums), 'pruned_image_count': num_removed, } - webhook_queue.put(json.dumps(webhook_data)) + webhook_queue.put([namespace, repository], json.dumps(webhook_data)) return make_response('Updated', 204) @@ -330,14 +349,17 @@ def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) # TODO invalidate token? + profile.debug('Looking up public status of repository') is_public = model.repository_is_public(namespace, repository) if permission.can() or is_public: # We can't rely on permissions to tell us if a repo exists anymore + profile.debug('Looking up repository') repo = model.get_repository(namespace, repository) if not repo: abort(404, message='Unknown repository', issue='unknown-repo') all_images = [] + profile.debug('Retrieving repository images') for image in model.get_repository_images(namespace, repository): new_image_view = { 'id': image.docker_image_id, @@ -345,6 +367,7 @@ def get_repository_images(namespace, repository): } all_images.append(new_image_view) + profile.debug('Building repository image response') resp = make_response(json.dumps(all_images), 200) resp.mimetype = 'application/json' @@ -353,6 +376,7 @@ def get_repository_images(namespace, repository): 'namespace': namespace, } + profile.debug('Logging the pull to Mixpanel and the log system') if get_validated_oauth_token(): oauth_token = get_validated_oauth_token() metadata['oauth_token_id'] = oauth_token.id @@ -374,7 +398,7 @@ def get_repository_images(namespace, repository): 'repository': '%s/%s' % (namespace, repository), } - mixpanel.track(pull_username, 'pull_repo', extra_params) + analytics.track(pull_username, 'pull_repo', extra_params) model.log_action('pull_repo', namespace, performer=get_authenticated_user(), ip=request.remote_addr, metadata=metadata, @@ -408,4 +432,5 @@ def get_search(): def ping(): response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' + response.headers['X-Docker-Registry-Standalone'] = '0' return response diff --git a/endpoints/registry.py b/endpoints/registry.py index 9727d5ccb..d701fd140 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -9,7 +9,7 @@ from time import time from data.queue import image_diff_queue -from app import app +from app import storage as store from auth.auth import process_auth, extract_namespace_repo_from_session from util import checksums, changes from util.http import abort @@ -17,11 +17,11 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission) from data import model + registry = Blueprint('registry', __name__) -store = app.config['STORAGE'] logger = logging.getLogger(__name__) - +profile = logging.getLogger('application.profiler') class SocketReader(object): def __init__(self, fp): @@ -40,16 +40,35 @@ class SocketReader(object): return buf +def image_is_uploading(namespace, repository, image_id, repo_image): + if repo_image and repo_image.storage and repo_image.storage.uploading is not None: + return repo_image.storage.uploading + + logger.warning('Setting legacy upload flag') + uuid = repo_image and repo_image.storage and repo_image.storage.uuid + mark_path = store.image_mark_path(namespace, repository, image_id, uuid) + return store.exists(mark_path) + + +def mark_upload_complete(namespace, repository, image_id, repo_image): + if repo_image and repo_image.storage and repo_image.storage.uploading is not None: + repo_image.storage.uploading = False + repo_image.storage.save() + else: + logger.warning('Removing legacy upload flag') + uuid = repo_image and repo_image.storage and repo_image.storage.uuid + mark_path = store.image_mark_path(namespace, repository, image_id, uuid) + if store.exists(mark_path): + store.remove(mark_path) + + def require_completion(f): """This make sure that the image push correctly finished.""" @wraps(f) def wrapper(namespace, repository, *args, **kwargs): image_id = kwargs['image_id'] repo_image = model.get_repo_image(namespace, repository, image_id) - uuid = repo_image and repo_image.storage and repo_image.storage.uuid - - if store.exists(store.image_mark_path(namespace, repository, image_id, - uuid)): + if image_is_uploading(namespace, repository, image_id, repo_image): abort(400, 'Image %(image_id)s is being uploaded, retry later', issue='upload-in-progress', image_id=kwargs['image_id']) @@ -88,17 +107,28 @@ def set_cache_headers(f): @set_cache_headers def get_image_layer(namespace, repository, image_id, headers): permission = ReadRepositoryPermission(namespace, repository) + + profile.debug('Checking repo permissions') if permission.can() or model.repository_is_public(namespace, repository): + profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) + uuid = repo_image and repo_image.storage and repo_image.storage.uuid + profile.debug('Looking up the layer path') path = store.image_layer_path(namespace, repository, image_id, uuid) + + profile.debug('Looking up the direct download URL') direct_download_url = store.get_direct_download_url(path) + if direct_download_url: + profile.debug('Returning direct download URL') return redirect(direct_download_url) try: + profile.debug('Streaming layer data') return Response(store.stream_read(path), headers=headers) except IOError: + profile.debug('Image not found') abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id) @@ -109,25 +139,32 @@ def get_image_layer(namespace, repository, image_id, headers): @process_auth @extract_namespace_repo_from_session def put_image_layer(namespace, repository, image_id): + profile.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) if not permission.can(): abort(403) + profile.debug('Retrieving image') repo_image = model.get_repo_image(namespace, repository, image_id) + uuid = repo_image and repo_image.storage and repo_image.storage.uuid try: + profile.debug('Retrieving image data') json_data = store.get_content(store.image_json_path(namespace, repository, image_id, uuid)) except IOError: abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id) + profile.debug('Retrieving image path info') layer_path = store.image_layer_path(namespace, repository, image_id, uuid) - mark_path = store.image_mark_path(namespace, repository, image_id, uuid) - if store.exists(layer_path) and not store.exists(mark_path): + if (store.exists(layer_path) and not + image_is_uploading(namespace, repository, image_id, repo_image)): abort(409, 'Image already exists', issue='image-exists', image_id=image_id) + profile.debug('Storing layer data') + input_stream = request.stream if request.headers.get('transfer-encoding') == 'chunked': # Careful, might work only with WSGI servers supporting chunked @@ -174,12 +211,12 @@ def put_image_layer(namespace, repository, image_id): issue='checksum-mismatch', image_id=image_id) # Checksum is ok, we remove the marker - store.remove(mark_path) + mark_upload_complete(namespace, repository, image_id, repo_image) # The layer is ready for download, send a job to the work queue to # process it. - logger.debug('Queing diffs job for image: %s' % image_id) - image_diff_queue.put(json.dumps({ + profile.debug('Adding layer to diff queue') + image_diff_queue.put([namespace, repository, image_id], json.dumps({ 'namespace': namespace, 'repository': repository, 'image_id': image_id, @@ -192,6 +229,7 @@ def put_image_layer(namespace, repository, image_id): @process_auth @extract_namespace_repo_from_session def put_image_checksum(namespace, repository, image_id): + profile.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) if not permission.can(): abort(403) @@ -204,17 +242,22 @@ def put_image_checksum(namespace, repository, image_id): abort(400, 'Checksum not found in Cookie for image %(imaage_id)s', issue='missing-checksum-cookie', image_id=image_id) + profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) + uuid = repo_image and repo_image.storage and repo_image.storage.uuid + + profile.debug('Looking up repo layer data') if not store.exists(store.image_json_path(namespace, repository, image_id, uuid)): abort(404, 'Image not found: %(image_id)s', issue='unknown-image', image_id=image_id) - mark_path = store.image_mark_path(namespace, repository, image_id, uuid) - if not store.exists(mark_path): + profile.debug('Marking image path') + if not image_is_uploading(namespace, repository, image_id, repo_image): abort(409, 'Cannot set checksum for image %(image_id)s', issue='image-write-error', image_id=image_id) + profile.debug('Storing image checksum') err = store_checksum(namespace, repository, image_id, uuid, checksum) if err: abort(400, err) @@ -227,12 +270,12 @@ def put_image_checksum(namespace, repository, image_id): issue='checksum-mismatch', image_id=image_id) # Checksum is ok, we remove the marker - store.remove(mark_path) + mark_upload_complete(namespace, repository, image_id, repo_image) # The layer is ready for download, send a job to the work queue to # process it. - logger.debug('Queing diffs job for image: %s' % image_id) - image_diff_queue.put(json.dumps({ + profile.debug('Adding layer to diff queue') + image_diff_queue.put([namespace, repository, image_id], json.dumps({ 'namespace': namespace, 'repository': repository, 'image_id': image_id, @@ -247,27 +290,31 @@ def put_image_checksum(namespace, repository, image_id): @require_completion @set_cache_headers def get_image_json(namespace, repository, image_id, headers): + profile.debug('Checking repo permissions') permission = ReadRepositoryPermission(namespace, repository) if not permission.can() and not model.repository_is_public(namespace, repository): abort(403) + profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) uuid = repo_image and repo_image.storage and repo_image.storage.uuid + profile.debug('Looking up repo layer data') try: data = store.get_content(store.image_json_path(namespace, repository, image_id, uuid)) except IOError: flask_abort(404) + profile.debug('Looking up repo layer size') try: - size = store.get_size(store.image_layer_path(namespace, repository, - image_id, uuid)) + size = repo_image.image_size or repo_image.storage.image_size headers['X-Docker-Size'] = str(size) except OSError: pass + profile.debug('Retrieving checksum') checksum_path = store.image_checksum_path(namespace, repository, image_id, uuid) if store.exists(checksum_path): @@ -284,14 +331,17 @@ def get_image_json(namespace, repository, image_id, headers): @require_completion @set_cache_headers def get_image_ancestry(namespace, repository, image_id, headers): + profile.debug('Checking repo permissions') permission = ReadRepositoryPermission(namespace, repository) if not permission.can() and not model.repository_is_public(namespace, repository): abort(403) + profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) uuid = repo_image and repo_image.storage and repo_image.storage.uuid + profile.debug('Looking up image data') try: data = store.get_content(store.image_ancestry_path(namespace, repository, image_id, uuid)) @@ -299,8 +349,11 @@ def get_image_ancestry(namespace, repository, image_id, headers): abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id) + profile.debug('Converting to <-> from JSON') response = make_response(json.dumps(json.loads(data)), 200) response.headers.extend(headers) + + profile.debug('Done') return response @@ -335,10 +388,12 @@ def store_checksum(namespace, repository, image_id, uuid, checksum): @process_auth @extract_namespace_repo_from_session def put_image_json(namespace, repository, image_id): + profile.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) if not permission.can(): abort(403) + profile.debug('Parsing image JSON') try: data = json.loads(request.data) except json.JSONDecodeError: @@ -351,6 +406,7 @@ def put_image_json(namespace, repository, image_id): abort(400, 'Missing key `id` in JSON for image: %(image_id)s', issue='invalid-request', image_id=image_id) + profile.debug('Looking up repo image') repo_image = model.get_repo_image(namespace, repository, image_id) uuid = repo_image and repo_image.storage and repo_image.storage.uuid @@ -358,12 +414,14 @@ def put_image_json(namespace, repository, image_id): checksum = request.headers.get('X-Docker-Checksum') if checksum: # Storing the checksum is optional at this stage + profile.debug('Storing image checksum') err = store_checksum(namespace, repository, image_id, uuid, checksum) if err: abort(400, err, issue='write-error') else: # We cleanup any old checksum in case it's a retry after a fail + profile.debug('Cleanup old checksum') store.remove(store.image_checksum_path(namespace, repository, image_id, uuid)) if image_id != data['id']: @@ -374,19 +432,27 @@ def put_image_json(namespace, repository, image_id): parent_image = None if parent_id: + profile.debug('Looking up parent image') parent_image = model.get_repo_image(namespace, repository, parent_id) + parent_uuid = (parent_image and parent_image.storage and parent_image.storage.uuid) + if parent_id: + profile.debug('Looking up parent image data') + if (parent_id and not store.exists(store.image_json_path(namespace, repository, parent_id, parent_uuid))): abort(400, 'Image %(image_id)s depends on non existing parent image %(parent_id)s', issue='invalid-request', image_id=image_id, parent_id=parent_id) + profile.debug('Looking up image storage paths') json_path = store.image_json_path(namespace, repository, image_id, uuid) - mark_path = store.image_mark_path(namespace, repository, image_id, uuid) - if store.exists(json_path) and not store.exists(mark_path): + + profile.debug('Checking if image already exists') + if (store.exists(json_path) and not + image_is_uploading(namespace, repository, image_id, repo_image)): abort(409, 'Image already exists', issue='image-exists', image_id=image_id) # If we reach that point, it means that this is a new image or a retry @@ -394,13 +460,20 @@ def put_image_json(namespace, repository, image_id): # save the metadata command_list = data.get('container_config', {}).get('Cmd', None) command = json.dumps(command_list) if command_list else None + + profile.debug('Setting image metadata') model.set_image_metadata(image_id, namespace, repository, data.get('created'), data.get('comment'), command, parent_image) - store.put_content(mark_path, 'true') + + profile.debug('Putting json path') store.put_content(json_path, request.data) + + profile.debug('Generating image ancestry') generate_ancestry(namespace, repository, image_id, uuid, parent_id, parent_uuid) + + profile.debug('Done') return make_response('true', 200) diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 57e32df66..8114278d4 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -7,10 +7,9 @@ import base64 from github import Github, UnknownObjectException, GithubException from tempfile import SpooledTemporaryFile -from app import app +from app import app, userfiles as user_files -user_files = app.config['USERFILES'] client = app.config['HTTPCLIENT'] @@ -21,6 +20,10 @@ TARBALL_MIME = 'application/gzip' CHUNK_SIZE = 512 * 1024 +def should_skip_commit(message): + return '[skip build]' in message or '[build skip]' in message + + class BuildArchiveException(Exception): pass @@ -36,6 +39,9 @@ class TriggerDeactivationException(Exception): class ValidationRequestException(Exception): pass +class SkipRequestException(Exception): + pass + class EmptyRepositoryException(Exception): pass @@ -160,7 +166,7 @@ class GithubBuildTrigger(BuildTrigger): try: hook = to_add_webhook.create_hook('web', webhook_config) config['hook_id'] = hook.id - config['master_branch'] = to_add_webhook.master_branch + config['master_branch'] = to_add_webhook.default_branch except GithubException: msg = 'Unable to create webhook on repository: %s' raise TriggerActivationException(msg % new_build_source) @@ -219,7 +225,7 @@ class GithubBuildTrigger(BuildTrigger): try: repo = gh_client.get_repo(source) - default_commit = repo.get_branch(repo.master_branch or 'master').commit + default_commit = repo.get_branch(repo.default_branch or 'master').commit commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) return [os.path.dirname(elem.path) for elem in commit_tree.tree @@ -240,7 +246,7 @@ class GithubBuildTrigger(BuildTrigger): gh_client = self._get_client(auth_token) try: repo = gh_client.get_repo(source) - master_branch = repo.master_branch or 'master' + master_branch = repo.default_branch or 'master' return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path) except GithubException as ge: return None @@ -292,7 +298,7 @@ class GithubBuildTrigger(BuildTrigger): # compute the tag(s) branch = ref.split('/')[-1] tags = {branch} - if branch == repo.master_branch: + if branch == repo.default_branch: tags.add('latest') logger.debug('Pushing to tags: %s' % tags) @@ -309,13 +315,20 @@ class GithubBuildTrigger(BuildTrigger): def handle_trigger_request(self, request, auth_token, config): payload = request.get_json() - + if not payload: + raise SkipRequestException() + if 'zen' in payload: raise ValidationRequestException() logger.debug('Payload %s', payload) ref = payload['ref'] commit_sha = payload['head_commit']['id'] + commit_message = payload['head_commit'].get('message', '') + + if should_skip_commit(commit_message): + raise SkipRequestException() + short_sha = GithubBuildTrigger.get_display_name(commit_sha) gh_client = self._get_client(auth_token) @@ -334,9 +347,9 @@ class GithubBuildTrigger(BuildTrigger): gh_client = self._get_client(auth_token) repo = gh_client.get_repo(source) - master = repo.get_branch(repo.master_branch) + master = repo.get_branch(repo.default_branch) master_sha = master.commit.sha short_sha = GithubBuildTrigger.get_display_name(master_sha) - ref = 'refs/heads/%s' % repo.master_branch + ref = 'refs/heads/%s' % repo.default_branch return self._prepare_build(config, repo, master_sha, short_sha, ref) diff --git a/endpoints/web.py b/endpoints/web.py index bc7a8f4b3..560fac724 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -1,26 +1,27 @@ import logging -import stripe import os from flask import (abort, redirect, request, url_for, make_response, Response, - Blueprint) + Blueprint, send_from_directory) from flask.ext.login import current_user from urlparse import urlparse from data import model from data.model.oauth import DatabaseAuthorizationProvider -from app import app +from app import app, billing as stripe from auth.auth import require_session_login from auth.permissions import AdministerOrganizationPermission from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache -from endpoints.common import common_login, render_page_template +from endpoints.common import common_login, render_page_template, route_show_if, route_hide_if from endpoints.csrf import csrf_protect, generate_csrf_token from util.names import parse_repository_name from util.gravatar import compute_hash from auth import scopes +import features + logger = logging.getLogger(__name__) web = Blueprint('web', __name__) @@ -55,6 +56,7 @@ def snapshot(path = ''): @web.route('/plans/') @no_cache +@route_show_if(features.BILLING) def plans(): return index('') @@ -83,6 +85,12 @@ def organizations(): def user(): return index('') +@web.route('/superuser/') +@no_cache +@route_show_if(features.SUPER_USERS) +def superuser(): + return index('') + @web.route('/signin/') @no_cache @@ -152,7 +160,14 @@ def privacy(): return render_page_template('privacy.html') +@web.route('/robots.txt', methods=['GET']) +@no_cache +def robots(): + return send_from_directory('static', 'robots.txt') + + @web.route('/receipt', methods=['GET']) +@route_show_if(features.BILLING) @require_session_login def receipt(): if not current_user.is_authenticated(): @@ -298,7 +313,8 @@ def request_authorization_code(): if not current_app: abort(404) - return provider._make_redirect_error_response(current_app.redirect_uri, 'redirect_uri_mismatch') + return provider._make_redirect_error_response(current_app.redirect_uri, + 'redirect_uri_mismatch') # Load the scope information. scope_info = scopes.get_scope_information(scope) @@ -320,8 +336,9 @@ def request_authorization_code(): # Show the authorization page. return render_page_template('oauthorize.html', scopes=scope_info, application=oauth_app_view, - enumerate=enumerate, client_id=client_id, redirect_uri=redirect_uri, - scope=scope, csrf_token_val=generate_csrf_token()) + enumerate=enumerate, client_id=client_id, + redirect_uri=redirect_uri, scope=scope, + csrf_token_val=generate_csrf_token()) if response_type == 'token': return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope) diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 93d5e413c..b31ad343d 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -1,18 +1,17 @@ import logging -import stripe import json from flask import request, make_response, Blueprint +from app import billing as stripe from data import model -from data.queue import dockerfile_build_queue from auth.auth import process_auth from auth.permissions import ModifyRepositoryPermission from util.invoice import renderInvoiceToHtml -from util.email import send_invoice_email +from util.email import send_invoice_email, send_subscription_change, send_payment_failed from util.names import parse_repository_name from util.http import abort -from endpoints.trigger import BuildTrigger, ValidationRequestException +from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException from endpoints.common import start_build @@ -26,22 +25,44 @@ def stripe_webhook(): request_data = request.get_json() logger.debug('Stripe webhook call: %s' % request_data) + customer_id = request_data.get('data', {}).get('object', {}).get('customer', None) + user = model.get_user_or_org_by_customer_id(customer_id) if customer_id else None + event_type = request_data['type'] if 'type' in request_data else None if event_type == 'charge.succeeded': - data = request_data['data'] if 'data' in request_data else {} - obj = data['object'] if 'object' in data else {} - invoice_id = obj['invoice'] if 'invoice' in obj else None - customer_id = obj['customer'] if 'customer' in obj else None + invoice_id = request_data['data']['object']['invoice'] - if invoice_id and customer_id: - # Find the user associated with the customer ID. - user = model.get_user_or_org_by_customer_id(customer_id) - if user and user.invoice_email: - # Lookup the invoice. - invoice = stripe.Invoice.retrieve(invoice_id) - if invoice: - invoice_html = renderInvoiceToHtml(invoice, user) - send_invoice_email(user.email, invoice_html) + if user and user.invoice_email: + # Lookup the invoice. + invoice = stripe.Invoice.retrieve(invoice_id) + if invoice: + invoice_html = renderInvoiceToHtml(invoice, user) + send_invoice_email(user.email, invoice_html) + + elif event_type.startswith('customer.subscription.'): + cust_email = user.email if user is not None else 'unknown@domain.com' + quay_username = user.username if user is not None else 'unknown' + + change_type = '' + if event_type.endswith('.deleted'): + plan_id = request_data['data']['object']['plan']['id'] + change_type = 'canceled %s' % plan_id + send_subscription_change(change_type, customer_id, cust_email, quay_username) + elif event_type.endswith('.created'): + plan_id = request_data['data']['object']['plan']['id'] + change_type = 'subscribed %s' % plan_id + send_subscription_change(change_type, customer_id, cust_email, quay_username) + elif event_type.endswith('.updated'): + if 'previous_attributes' in request_data['data']: + if 'plan' in request_data['data']['previous_attributes']: + old_plan = request_data['data']['previous_attributes']['plan']['id'] + new_plan = request_data['data']['object']['plan']['id'] + change_type = 'switched %s -> %s' % (old_plan, new_plan) + send_subscription_change(change_type, customer_id, cust_email, quay_username) + + elif event_type == 'invoice.payment_failed': + if user: + send_payment_failed(user.email, user.username) return make_response('Okay') @@ -73,6 +94,10 @@ def build_trigger_webhook(namespace, repository, trigger_uuid): # This was just a validation request, we don't need to build anything return make_response('Okay') + except SkipRequestException: + # The build was requested to be skipped + return make_response('Okay') + pull_robot_name = model.get_pull_robot_name(trigger) repo = model.get_repository(namespace, repository) start_build(repo, dockerfile_id, tags, name, subdir, False, trigger, diff --git a/features/__init__.py b/features/__init__.py new file mode 100644 index 000000000..9318a9b4a --- /dev/null +++ b/features/__init__.py @@ -0,0 +1,31 @@ +_FEATURES = {} + +def import_features(config_dict): + for feature, feature_val in config_dict.items(): + if feature.startswith('FEATURE_'): + feature_name = feature[8:] + _FEATURES[feature_name] = globals()[feature_name] = FeatureNameValue(feature_name, feature_val) + + +def get_features(): + return {key: _FEATURES[key].value for key in _FEATURES} + + +class FeatureNameValue(object): + def __init__(self, name, value): + self.value = value + self.name = name + + def __str__(self): + return '%s => %s' % (self.name, self.value) + + def __repr__(self): + return str(self.value) + + def __cmp__(self, other): + return self.value.__cmp__(other) + + def __nonzero__(self): + return self.value.__nonzero__() + + diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js new file mode 100644 index 000000000..218bb6a9d --- /dev/null +++ b/grunt/Gruntfile.js @@ -0,0 +1,80 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + process: function(src, filepath) { + var unwraps = ['/js/']; + + var shouldWrap = true; + for (var i = 0; i < unwraps.length; ++i) { + if (filepath.indexOf(unwraps[i]) >= 0) { + shouldWrap = false; + break; + } + } + + if (shouldWrap) { + return '// Source: ' + filepath + '\n' + + '(function() {\n' + src + '\n})();\n'; + } else { + return '// Source: ' + filepath + '\n' + src + '\n\n'; + } + }, + }, + build: { + src: ['../static/lib/**/*.js', '../static/js/*.js', '../static/dist/template-cache.js'], + dest: '../static/dist/<%= pkg.name %>.js' + } + }, + + cssmin: { + '../static/dist/<%= pkg.name %>.css': ['../static/lib/**/*.css', '../static/css/*.css'] + }, + + uglify: { + options: { + mangle: false, + sourceMap: true, + sourceMapName: '../static/dist/<%= pkg.name %>.min.map' + }, + js_min: { + files: { + '../static/dist/<%= pkg.name %>.min.js': ['../static/dist/<%= pkg.name %>.js'] + } + } + }, + + ngtemplates: { + options: { + url: function(path) { + return '/' + path.substr(3); // remove the ../ + }, + htmlmin: { + collapseBooleanAttributes: true, + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, // Only if you don't use comment directives! + removeEmptyAttributes: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true + } + }, + quay: { + src: ['../static/partials/*.html', '../static/directives/*.html'], + dest: '../static/dist/template-cache.js' + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.loadNpmTasks('grunt-angular-templates'); + + // Default task(s). + grunt.registerTask('default', ['ngtemplates', 'concat', 'cssmin', 'uglify']); +}; \ No newline at end of file diff --git a/grunt/package.json b/grunt/package.json new file mode 100644 index 000000000..e4d9836a3 --- /dev/null +++ b/grunt/package.json @@ -0,0 +1,11 @@ +{ + "name": "quay-frontend", + "version": "0.1.0", + "devDependencies": { + "grunt": "~0.4.4", + "grunt-contrib-concat": "~0.4.0", + "grunt-contrib-cssmin": "~0.9.0", + "grunt-angular-templates": "~0.5.4", + "grunt-contrib-uglify": "~0.4.0" + } +} diff --git a/initdb.py b/initdb.py index 3ddb601f3..2570b7ca9 100644 --- a/initdb.py +++ b/initdb.py @@ -10,11 +10,10 @@ from peewee import (SqliteDatabase, create_model_tables, drop_model_tables, from data.database import * from data import model from data.model import oauth -from app import app +from app import app, storage as store logger = logging.getLogger(__name__) -store = app.config['STORAGE'] SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i for i in range(1, 10)] @@ -149,8 +148,7 @@ def setup_database_for_testing(testcase): # Sanity check to make sure we're not killing our prod db db = model.db - if (not isinstance(model.db, SqliteDatabase) or - app.config['DB_DRIVER'] is not SqliteDatabase): + if not isinstance(model.db, SqliteDatabase): raise RuntimeError('Attempted to wipe production database!') global db_initialized_for_testing @@ -198,6 +196,8 @@ def initialize_database(): LogEntryKind.create(name='push_repo') LogEntryKind.create(name='pull_repo') LogEntryKind.create(name='delete_repo') + LogEntryKind.create(name='create_tag') + LogEntryKind.create(name='move_tag') LogEntryKind.create(name='delete_tag') LogEntryKind.create(name='add_repo_permission') LogEntryKind.create(name='change_repo_permission') @@ -241,8 +241,7 @@ def wipe_database(): # Sanity check to make sure we're not killing our prod db db = model.db - if (not isinstance(model.db, SqliteDatabase) or - app.config['DB_DRIVER'] is not SqliteDatabase): + if not isinstance(model.db, SqliteDatabase): raise RuntimeError('Attempted to wipe production database!') drop_model_tables(all_models, fail_silently=True) @@ -490,7 +489,8 @@ def populate_database(): 'service': trigger.service.name}) if __name__ == '__main__': - app.config['LOGGING_CONFIG']() + log_level = getattr(logging, app.config['LOGGING_LEVEL']) + logging.basicConfig(level=log_level) initialize_database() if app.config.get('POPULATE_DB_TEST_DATA', False): diff --git a/requirements-nover.txt b/requirements-nover.txt index 8bd0d7946..cc370da9d 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -18,7 +18,6 @@ python-daemon paramiko python-digitalocean xhtml2pdf -logstash_formatter redis hiredis git+https://github.com/DevTable/docker-py.git @@ -26,4 +25,10 @@ loremipsum pygithub flask-restful jsonschema -git+https://github.com/NateFerrero/oauth2lib.git \ No newline at end of file +git+https://github.com/NateFerrero/oauth2lib.git +alembic +sqlalchemy +python-magic +reportlab==2.7 +blinker +raven diff --git a/requirements.txt b/requirements.txt index 46def6790..7951f5dd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,47 +5,49 @@ Flask-Mail==0.9.0 Flask-Principal==0.4.0 Flask-RESTful==0.2.12 Jinja2==2.7.2 -MarkupSafe==0.19 -Pillow==2.3.1 +Mako==0.9.1 +MarkupSafe==0.21 +Pillow==2.4.0 PyGithub==1.24.1 -PyMySQL==0.6.1 +PyMySQL==0.6.2 +PyPDF2==1.21 +SQLAlchemy==0.9.4 Werkzeug==0.9.4 +alembic==0.6.4 aniso8601==0.82 argparse==1.2.1 beautifulsoup4==4.3.2 blinker==1.3 boto==2.27.0 -distribute==0.6.34 git+https://github.com/DevTable/docker-py.git ecdsa==0.11 -gevent==1.0 +gevent==1.0.1 greenlet==0.4.2 gunicorn==18.0 -hiredis==0.1.2 -html5lib==1.0b3 -itsdangerous==0.23 +hiredis==0.1.3 +html5lib==0.999 +itsdangerous==0.24 jsonschema==2.3.0 lockfile==0.9.1 -logstash-formatter==0.5.8 loremipsum==1.0.2 marisa-trie==0.6 mixpanel-py==3.1.2 -mock==1.0.1 git+https://github.com/NateFerrero/oauth2lib.git paramiko==1.13.0 -peewee==2.2.2 +peewee==2.2.3 py-bcrypt==0.4 -pyPdf==1.13 pycrypto==2.6.1 python-daemon==1.6 python-dateutil==2.2 python-digitalocean==0.7 +python-magic==0.4.6 pytz==2014.2 +raven==4.2.1 redis==2.9.1 reportlab==2.7 requests==2.2.1 six==1.6.1 -stripe==1.12.2 +stripe==1.14.0 websocket-client==0.11.0 wsgiref==0.1.2 -xhtml2pdf==0.0.5 +xhtml2pdf==0.0.6 diff --git a/static/css/quay.css b/static/css/quay.css index 1746d9bfe..e6cf04f1e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -676,6 +676,10 @@ i.toggle-icon:hover { background-color: #ddd; } +.phase-icon.pulling { + background-color: #cab442; +} + .phase-icon.building { background-color: #f0ad4e; } @@ -995,6 +999,24 @@ i.toggle-icon:hover { } } +.visible-xl { + display: none; +} + +.visible-xl-inline { + display: none; +} + +@media (min-width: 1200px) { + .visible-xl { + display: block; + } + + .visible-xl-inline { + display: inline-block; + } +} + .plans-list .plan-box .description { color: white; margin-top: 6px; @@ -1528,22 +1550,22 @@ p.editable:hover i { border: 0px; } -#confirmdeleteTagModal .image-listings { +.tag-specific-images-view .image-listings { margin: 10px; } -#confirmdeleteTagModal .image-listings .image-listing { +.tag-specific-images-view .image-listings .image-listing { margin: 4px; padding: 2px; position: relative; } -#confirmdeleteTagModal .image-listings .image-listing .image-listing-id { +.tag-specific-images-view .image-listings .image-listing .image-listing-id { display: inline-block; margin-left: 20px; } -#confirmdeleteTagModal .image-listings .image-listing .image-listing-line { +.tag-specific-images-view .image-listings .image-listing .image-listing-line { border-left: 2px solid steelblue; display: inline-block; position: absolute; @@ -1554,15 +1576,15 @@ p.editable:hover i { z-index: 1; } -#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-line { +.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-line { top: 8px; } -#confirmdeleteTagModal .image-listings .image-listing.child .image-listing-line { +.tag-specific-images-view .image-listings .image-listing.child .image-listing-line { bottom: -2px; } -#confirmdeleteTagModal .image-listings .image-listing .image-listing-circle { +.tag-specific-images-view .image-listings .image-listing .image-listing-circle { position: absolute; top: 8px; @@ -1575,14 +1597,55 @@ p.editable:hover i { z-index: 2; } -#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-circle { +.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-circle { background: steelblue; } -#confirmdeleteTagModal .more-changes { +.tag-specific-images-view .more-changes { margin-left: 16px; } +.repo.container-fluid { + padding-left: 10px; + padding-right: 10px; +} + +@media (min-width: 768px) { + .repo.container-fluid { + padding-left: 20px; + padding-right: 20px; + } +} + +@media (min-width: 1200px) { + .repo.container-fluid { + padding-left: 40px; + padding-right: 40px; + } + + .repo.container-fluid .col-md-4 { + width: 30%; + } + + .repo.container-fluid .col-md-8 { + width: 70%; + } +} + + +.repo .current-context { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; + vertical-align: middle; +} + +.repo .current-context-icon { + vertical-align: middle; + margin-right: 4px; +} + .repo .header { margin-bottom: 20px; padding-bottom: 16px; @@ -1644,6 +1707,10 @@ p.editable:hover i { display: inline-block; } +.repo .repo-controls .dropdown { + margin-right: 10px; +} + .repo .repo-controls .count { display: inline-block; padding-left: 4px; @@ -1798,6 +1865,77 @@ p.editable:hover i { text-decoration: none !important; } +.repo .image-comment { + margin-bottom: 4px; +} + +.repo .image-section { + margin-top: 12px; + padding-bottom: 2px; + position: relative; +} + +.repo .image-section .tag { + margin: 2px; + border-radius: 8px; + display: inline-block; + padding: 4px; +} + + +.repo .image-section .section-icon { + float: left; + font-size: 16px; + margin-left: -4px; + margin-right: 14px; + color: #bbb; + width: 18px; + text-align: center; + padding-top: 6px; +} + +.repo .image-section .section-info { + padding: 4px; + padding-left: 6px; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05); + box-shadow: inset 0 1px 1px rgba(0,0,0,0.05); + background-color: #f5f5f5; + + vertical-align: middle; + display: block; + overflow: hidden; + text-overflow: ellipsis; + border-radius: 6px; +} + +.repo .image-section .section-info-with-dropdown { + padding-right: 30px; +} + +.repo .image-section .dropdown { + display: inline-block; + position: absolute; + top: 0px; + bottom: 2px; + right: 0px; +} + +.repo .image-section .dropdown-button { + position: absolute; + right: 0px; + top: 0px; + bottom: 0px; + + background: white; + padding: 4px; + padding-left: 8px; + padding-right: 8px; + border: 1px solid #eee; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + cursor: pointer; +} + .repo-list { margin-bottom: 40px; } @@ -2106,19 +2244,11 @@ p.editable:hover i { margin: 0px; } -.repo .small-changes-container:before { - content: "File Changes: "; - display: inline-block; - margin-right: 10px; - font-weight: bold; - float: left; - padding-top: 4px; -} - .repo .formatted-command { margin-top: 4px; padding: 4px; font-size: 12px; + font-family: Consolas, "Lucida Console", Monaco, monospace; } .repo .formatted-command.trimmed { @@ -2127,16 +2257,22 @@ p.editable:hover i { text-overflow: ellipsis; } -.repo .changes-count-container { - text-align: center; -} - .repo .change-count { - font-size: 18px; + font-size: 16px; display: inline-block; margin-right: 10px; } +.repo .change-count b { + font-weight: normal; + margin-left: 6px; + vertical-align: middle; +} + +.repo .changes-container .well { + border: 0px; +} + .repo .changes-container i.fa-plus-square { color: rgb(73, 209, 73); } @@ -2154,7 +2290,7 @@ p.editable:hover i { } .repo .change-count i { - font-size: 20px; + font-size: 16px; vertical-align: middle; } @@ -2166,6 +2302,7 @@ p.editable:hover i { .repo .more-changes { padding: 6px; + text-align: right; } .repo #collapseChanges .well { @@ -2316,11 +2453,6 @@ p.editable:hover i { margin-bottom: 10px; } -.user-admin .form-change input { - margin-top: 12px; - margin-bottom: 12px; -} - .user-admin .convert-form h3 { margin-bottom: 20px; } @@ -2411,10 +2543,13 @@ p.editable:hover i { text-align: center; } -.tags .tag, #confirmdeleteTagModal .tag { +.tags .tag, .tag-specific-images-view .tag { + display: inline-block; border-radius: 10px; margin-right: 4px; cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; } .tooltip-tags { @@ -2464,42 +2599,42 @@ p.editable:hover i { stroke-width: 1.5px; } -#repository-usage-chart { +.usage-chart { display: inline-block; vertical-align: middle; width: 200px; height: 200px; } -#repository-usage-chart .count-text { +.usage-chart .count-text { font-size: 22px; } -#repository-usage-chart.limit-at path.arc-0 { +.usage-chart.limit-at path.arc-0 { fill: #c09853; } -#repository-usage-chart.limit-over path.arc-0 { +.usage-chart.limit-over path.arc-0 { fill: #b94a48; } -#repository-usage-chart.limit-near path.arc-0 { +.usage-chart.limit-near path.arc-0 { fill: #468847; } -#repository-usage-chart.limit-over path.arc-1 { +.usage-chart.limit-over path.arc-1 { fill: #fcf8e3; } -#repository-usage-chart.limit-at path.arc-1 { +.usage-chart.limit-at path.arc-1 { fill: #f2dede; } -#repository-usage-chart.limit-near path.arc-1 { +.usage-chart.limit-near path.arc-1 { fill: #dff0d8; } -.plan-manager-element .usage-caption { +.usage-caption { display: inline-block; color: #aaa; font-size: 26px; @@ -3630,4 +3765,17 @@ pre.command:before { .trigger-option-section table td { padding: 6px; +} + +.user-row.super-user td { + background-color: #d9edf7; +} + +.user-row .user-class { + text-transform: uppercase; +} + +.form-change input { + margin-top: 12px; + margin-bottom: 12px; } \ No newline at end of file diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 8c2aeb8ea..438e2789e 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -14,7 +14,7 @@
  • Repositories
  • Docs
  • Tutorial
  • -
  • Pricing
  • +
  • Pricing
  • Organizations
  • @@ -65,6 +65,7 @@
  • Organizations
  • +
  • Super User Admin Panel
  • Sign out
  • diff --git a/static/directives/plan-manager.html b/static/directives/plan-manager.html index ee849f64d..af4c3c016 100644 --- a/static/directives/plan-manager.html +++ b/static/directives/plan-manager.html @@ -17,10 +17,15 @@ You are nearing the number of allowed private repositories. It might be time to think about upgrading your subscription to avoid future disruptions in your organization's service. + + +
    + Free trial until {{ parseDate(subscription.trialEnd) | date }} +
    -
    +
    Repository Usage
    @@ -57,7 +62,8 @@ ng-click="changeSubscription(plan.stripeId)"> Change - Subscribe + Start Free Trial + Subscribe diff --git a/static/directives/setup-trigger-dialog.html b/static/directives/setup-trigger-dialog.html index 1e84a782a..2be52ee4e 100644 --- a/static/directives/setup-trigger-dialog.html +++ b/static/directives/setup-trigger-dialog.html @@ -8,7 +8,10 @@ - + + + + + + + + + + + diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 1be96046e..25ef0ec32 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -20,15 +20,24 @@ @@ -91,12 +100,12 @@ -
    +
    -
    +
    An e-mail has been sent to {{ sentEmail }} to verify the change.
    @@ -152,7 +161,7 @@
    -
    +
    @@ -178,12 +187,12 @@
    -
    +
    -
    +
    @@ -235,13 +244,14 @@
    -
    +
    - diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index 6d02dae54..b5d039974 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -5,7 +5,7 @@
    -
    +

    @@ -58,7 +58,7 @@
    - + @@ -87,13 +87,13 @@
    How to push a new image to this repository:
    First login to Quay.io (if you have not done so already): -
    sudo docker login quay.io
    +
    sudo docker login {{ Config.getDomain() }}
    Tag an image to this repository: -
    sudo docker tag 0u123imageidgoeshere quay.io/{{repo.namespace}}/{{repo.name}}
    +
    sudo docker tag 0u123imageidgoeshere {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}
    Push the image to this repository: -
    sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}
    +
    sudo docker push {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}
    @@ -127,9 +127,13 @@