Merge branch 'master' of bitbucket.org:yackob03/quay
This commit is contained in:
commit
acbfc0bd5a
143 changed files with 4373 additions and 31056 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -2,3 +2,8 @@
|
||||||
venv
|
venv
|
||||||
static/snapshots/
|
static/snapshots/
|
||||||
screenshots/screenshots/
|
screenshots/screenshots/
|
||||||
|
stack
|
||||||
|
grunt/node_modules
|
||||||
|
dist
|
||||||
|
dest
|
||||||
|
node_modules
|
||||||
|
|
72
Dockerfile
Normal file
72
Dockerfile
Normal file
|
@ -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"]
|
83
README.md
83
README.md
|
@ -1,64 +1,55 @@
|
||||||
to prepare a new host:
|
to build and upload quay to quay:
|
||||||
|
|
||||||
```
|
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -s https://get.docker.io/ubuntu/ | sudo sh
|
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 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
|
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 <pids of worker daemons>
|
|
||||||
restart daemons
|
|
||||||
```
|
|
||||||
|
|
||||||
running the tests:
|
running the tests:
|
||||||
|
|
||||||
```
|
```
|
||||||
STACK=test python -m unittest discover
|
TEST=true python -m unittest discover
|
||||||
```
|
```
|
||||||
|
|
||||||
running the tests with coverage (requires coverage module):
|
running the tests with coverage (requires coverage module):
|
||||||
|
|
||||||
```
|
```
|
||||||
STACK=test coverage run -m unittest discover
|
TEST=true coverage run -m unittest discover
|
||||||
coverage html
|
coverage html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
58
alembic.ini
Normal file
58
alembic.ini
Normal file
|
@ -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
|
58
app.py
58
app.py
|
@ -1,48 +1,48 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stripe
|
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.ext.principal import Principal
|
from flask.ext.principal import Principal
|
||||||
from flask.ext.login import LoginManager
|
from flask.ext.login import LoginManager
|
||||||
from flask.ext.mail import Mail
|
from flask.ext.mail import Mail
|
||||||
|
|
||||||
from config import (ProductionConfig, DebugConfig, LocalHostedConfig,
|
import features
|
||||||
TestConfig, StagingConfig)
|
|
||||||
from util import analytics
|
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__)
|
app = Flask(__name__)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
stack = os.environ.get('STACK', '').strip().lower()
|
if 'TEST' in os.environ:
|
||||||
if stack.startswith('prod'):
|
from test.testconfig import TestConfig
|
||||||
logger.info('Running with production config.')
|
logger.debug('Loading test config.')
|
||||||
config = ProductionConfig()
|
app.config.from_object(TestConfig())
|
||||||
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()
|
|
||||||
else:
|
else:
|
||||||
logger.info('Running with debug config.')
|
from config import DefaultConfig
|
||||||
config = DebugConfig()
|
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)
|
Principal(app, use_sessions=False)
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager(app)
|
||||||
login_manager.init_app(app)
|
mail = Mail(app)
|
||||||
|
storage = Storage(app)
|
||||||
mail = Mail()
|
userfiles = Userfiles(app)
|
||||||
mail.init_app(app)
|
analytics = Analytics(app)
|
||||||
|
billing = Billing(app)
|
||||||
stripe.api_key = app.config.get('STRIPE_SECRET_KEY', None)
|
sentry = Sentry(app)
|
||||||
|
|
||||||
mixpanel = app.config['ANALYTICS'].init_app(app)
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import uuid
|
||||||
|
|
||||||
from app import app as application
|
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
|
from data.model import db as model_db
|
||||||
application.config['LOGGING_CONFIG']()
|
|
||||||
|
|
||||||
# Turn off debug logging for boto
|
# Turn off debug logging for boto
|
||||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||||
|
@ -20,6 +22,7 @@ from endpoints.callbacks import callback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
profile = logging.getLogger('application.profiler')
|
||||||
|
|
||||||
application.register_blueprint(web)
|
application.register_blueprint(web)
|
||||||
application.register_blueprint(callback, url_prefix='/oauth2')
|
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(webhooks, url_prefix='/webhooks')
|
||||||
application.register_blueprint(realtime, url_prefix='/realtime')
|
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):
|
def close_db(exc):
|
||||||
db = model_db
|
db = model_db
|
||||||
|
@ -38,6 +64,8 @@ def close_db(exc):
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
application.teardown_request(close_db)
|
application.teardown_request(close_db)
|
||||||
|
application.request_class = RequestWithId
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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')
|
application.run(port=5000, debug=True, threaded=True, host='0.0.0.0')
|
||||||
|
|
|
@ -22,6 +22,7 @@ _TeamTypeNeed = namedtuple('teamwideneed', ['type', 'orgname', 'teamname', 'role
|
||||||
_TeamNeed = partial(_TeamTypeNeed, 'orgteam')
|
_TeamNeed = partial(_TeamTypeNeed, 'orgteam')
|
||||||
_UserTypeNeed = namedtuple('userspecificneed', ['type', 'username', 'role'])
|
_UserTypeNeed = namedtuple('userspecificneed', ['type', 'username', 'role'])
|
||||||
_UserNeed = partial(_UserTypeNeed, 'user')
|
_UserNeed = partial(_UserTypeNeed, 'user')
|
||||||
|
_SuperUserNeed = partial(namedtuple('superuserneed', ['type']), 'superuser')
|
||||||
|
|
||||||
|
|
||||||
REPO_ROLES = [None, 'read', 'write', 'admin']
|
REPO_ROLES = [None, 'read', 'write', 'admin']
|
||||||
|
@ -88,6 +89,11 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
logger.debug('Loading user permissions after deferring.')
|
logger.debug('Loading user permissions after deferring.')
|
||||||
user_object = model.get_user(self.id)
|
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
|
# Add the user specific permissions, only for non-oauth permission
|
||||||
user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin'))
|
user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin'))
|
||||||
logger.debug('User permission: {0}'.format(user_grant))
|
logger.debug('User permission: {0}'.format(user_grant))
|
||||||
|
@ -171,6 +177,11 @@ class CreateRepositoryPermission(Permission):
|
||||||
super(CreateRepositoryPermission, self).__init__(admin_org,
|
super(CreateRepositoryPermission, self).__init__(admin_org,
|
||||||
create_repo_org)
|
create_repo_org)
|
||||||
|
|
||||||
|
class SuperUserPermission(Permission):
|
||||||
|
def __init__(self):
|
||||||
|
need = _SuperUserNeed()
|
||||||
|
super(SuperUserPermission, self).__init__(need)
|
||||||
|
|
||||||
|
|
||||||
class UserAdminPermission(Permission):
|
class UserAdminPermission(Permission):
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
binary_dependencies/nginx_1.4.2-nobuffer-2_amd64.deb
Normal file
BIN
binary_dependencies/nginx_1.4.2-nobuffer-2_amd64.deb
Normal file
Binary file not shown.
|
@ -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-----
|
|
|
@ -1 +0,0 @@
|
||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCOUgrQeh2YM2tkCZoAu2v1EuuJFJ54uHvqBXwoeaNHB55Pu92aEp4Y/Wc4CzXtxpmRzCxbS2INsJ4i/YyKxXjTmxJyM87EWK9aljy0vvayBW44CVF5lq44ZngzVlxJr9htPu2cUIhrJgT2l17iKHkuZhmoqwGxo2rcStwycDOobvsP3nyzC5XCHP8g2KgFbB7gWM7vr/QOpLfnuFPnIcNa2EkPpRQfXZVqM8oG1cS8Y1S00u92r52ub2ia3bLSx4yfjt9leI0i6Uu/uiOxYrC1ExOwZyOe/pUxkBUC5fXW7hwl36g9NsOXQP7TYHtdhzEAGp0xlw5zX/tVNSUUV3j jake@coreserver
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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-----
|
|
|
@ -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
|
|
27
conf/deploy
27
conf/deploy
|
@ -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-----
|
|
|
@ -1 +0,0 @@
|
||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDWpiOo8kA4fMT2PITTouA5gVe2ZVYww91LXShZQfvv8zjTr+UEbU1/xVTb+rtWiB9H+TnrG6aUBhygQ/2u54Qwj2PZHAPLazlIdpPtn71LJmdNG2puiZNQdUFC3GBFfPZcdE5a0UbngyFm0gcOTfWRbarKv6szDb+RPWHr6z0PWYXkDmINbE1BJZjJL2Bozlrlo1RWxyhcC1IbsnBjhd1gQYpfAybtRgV4eU48f9BFNlU71c9n+81bW3oln0YgDF8U/2/ZjJ6EXeXZH6bdUYOV3gk2ixQ/JY6cw9scFxNgZ7acuCPMf2aqFcpHSNHaFujRAnPmWfxNeaPm/eZ8+sXV jake@coreserver
|
|
|
@ -2,9 +2,6 @@ bind = 'unix:/tmp/gunicorn.sock'
|
||||||
workers = 8
|
workers = 8
|
||||||
worker_class = 'gevent'
|
worker_class = 'gevent'
|
||||||
timeout = 2000
|
timeout = 2000
|
||||||
daemon = True
|
pidfile = '/tmp/gunicorn.pid'
|
||||||
pidfile = '/mnt/logs/gunicorn.pid'
|
logconfig = 'conf/logging.conf'
|
||||||
errorlog = '/mnt/logs/application.log'
|
|
||||||
loglevel = 'debug'
|
|
||||||
logger_class = 'util.glogger.LogstashLogger'
|
|
||||||
pythonpath = '.'
|
pythonpath = '.'
|
|
@ -3,7 +3,5 @@ workers = 2
|
||||||
worker_class = 'gevent'
|
worker_class = 'gevent'
|
||||||
timeout = 2000
|
timeout = 2000
|
||||||
daemon = False
|
daemon = False
|
||||||
errorlog = '-'
|
logconfig = 'conf/logging_local.conf'
|
||||||
loglevel = 'debug'
|
|
||||||
logger_class = 'util.glogger.LogstashLogger'
|
|
||||||
pythonpath = '.'
|
pythonpath = '.'
|
8
conf/init/diffsworker.sh
Executable file
8
conf/init/diffsworker.sh
Executable file
|
@ -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'
|
8
conf/init/gunicorn.sh
Executable file
8
conf/init/gunicorn.sh
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
echo 'Starting gunicon'
|
||||||
|
|
||||||
|
cd /
|
||||||
|
venv/bin/gunicorn -c conf/gunicorn_config.py application:application
|
||||||
|
|
||||||
|
echo 'Gunicorn exited'
|
4
conf/init/mklogsdir.sh
Executable file
4
conf/init/mklogsdir.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
echo 'Creating logs directory'
|
||||||
|
mkdir -p /mnt/logs
|
14
conf/init/nginx.sh
Executable file
14
conf/init/nginx.sh
Executable file
|
@ -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'
|
8
conf/init/webhookworker.sh
Executable file
8
conf/init/webhookworker.sh
Executable file
|
@ -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'
|
39
conf/logging.conf
Normal file
39
conf/logging.conf
Normal file
|
@ -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
|
39
conf/logging_local.conf
Normal file
39
conf/logging_local.conf
Normal file
|
@ -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
|
|
@ -2,17 +2,21 @@ include root-base.conf;
|
||||||
|
|
||||||
worker_processes 2;
|
worker_processes 2;
|
||||||
|
|
||||||
|
user root nogroup;
|
||||||
|
|
||||||
|
daemon off;
|
||||||
|
|
||||||
http {
|
http {
|
||||||
include http-base.conf;
|
include http-base.conf;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
include server-base.conf;
|
include server-base.conf;
|
||||||
|
|
||||||
listen 5000 default;
|
listen 80 default;
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
# checks for static file, if not found proxy to app
|
# checks for static file, if not found proxy to app
|
||||||
alias /home/jake/Projects/docker/quay/static/;
|
alias /static/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,8 @@ worker_processes 2;
|
||||||
|
|
||||||
user root nogroup;
|
user root nogroup;
|
||||||
|
|
||||||
|
daemon off;
|
||||||
|
|
||||||
http {
|
http {
|
||||||
include http-base.conf;
|
include http-base.conf;
|
||||||
|
|
||||||
|
@ -15,8 +17,8 @@ http {
|
||||||
listen 443 default;
|
listen 443 default;
|
||||||
|
|
||||||
ssl on;
|
ssl on;
|
||||||
ssl_certificate ./certs/quay-staging-unified.cert;
|
ssl_certificate ./stack/ssl.cert;
|
||||||
ssl_certificate_key ./certs/quay-staging.key;
|
ssl_certificate_key ./stack/ssl.key;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
ssl_protocols SSLv3 TLSv1;
|
ssl_protocols SSLv3 TLSv1;
|
||||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
||||||
|
@ -24,7 +26,7 @@ http {
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
# checks for static file, if not found proxy to app
|
# checks for static file, if not found proxy to app
|
||||||
alias /root/quay/static/;
|
alias /static/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
pid /mnt/logs/nginx.pid;
|
pid /tmp/nginx.pid;
|
||||||
error_log /mnt/logs/nginx.error.log;
|
error_log /mnt/logs/nginx.error.log;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
|
|
350
config.py
350
config.py
|
@ -1,198 +1,8 @@
|
||||||
import logging
|
|
||||||
import logstash_formatter
|
|
||||||
import requests
|
import requests
|
||||||
import os.path
|
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.buildlogs import BuildLogs
|
||||||
from data.userevent import UserEventBuilder
|
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 = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
|
||||||
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():
|
def build_requests_session():
|
||||||
|
@ -204,70 +14,124 @@ def build_requests_session():
|
||||||
return sess
|
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()
|
HTTPCLIENT = build_requests_session()
|
||||||
|
|
||||||
|
# Status tag config
|
||||||
class StatusTagConfig(object):
|
|
||||||
STATUS_TAGS = {}
|
STATUS_TAGS = {}
|
||||||
|
|
||||||
for tag_name in ['building', 'failed', 'none', 'ready']:
|
for tag_name in ['building', 'failed', 'none', 'ready']:
|
||||||
tag_path = os.path.join('buildstatus', tag_name + '.svg')
|
tag_path = os.path.join('buildstatus', tag_name + '.svg')
|
||||||
with open(tag_path) as tag_svg:
|
with open(tag_path) as tag_svg:
|
||||||
STATUS_TAGS[tag_name] = tag_svg.read()
|
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,
|
# Super user config. Note: This MUST BE an empty list for the default config.
|
||||||
FakeAnalytics, StripeTestConfig, RedisBuildLogs,
|
SUPER_USERS = []
|
||||||
UserEventConfig, LargePoolHttpClient, StatusTagConfig):
|
|
||||||
LOGGING_CONFIG = logs_init_builder(logging.WARN)
|
|
||||||
POPULATE_DB_TEST_DATA = True
|
|
||||||
TESTING = True
|
|
||||||
URL_SCHEME = 'http'
|
|
||||||
URL_HOST = 'localhost:5000'
|
|
||||||
|
|
||||||
|
# Feature Flag: Whether billing is required.
|
||||||
|
FEATURE_BILLING = True
|
||||||
|
|
||||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
# Feature Flag: Whether user accounts automatically have usage log access.
|
||||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
FEATURE_USER_LOG_ACCESS = False
|
||||||
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 GitHub login is supported.
|
||||||
|
FEATURE_GITHUB_LOGIN = False
|
||||||
|
|
||||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
# Feature flag, whether to enable olark chat
|
||||||
StripeLiveConfig, MixpanelTestConfig,
|
FEATURE_OLARK_CHAT = False
|
||||||
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 super users are supported.
|
||||||
class StagingConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL,
|
FEATURE_SUPER_USERS = False
|
||||||
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'
|
|
||||||
|
|
234
data/billing.py
Normal file
234
data/billing.py
Normal file
|
@ -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)
|
|
@ -5,13 +5,39 @@ import uuid
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from peewee import *
|
from peewee import *
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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_generator(length=16):
|
||||||
def random_string():
|
def random_string():
|
||||||
|
@ -194,7 +220,8 @@ class ImageStorage(BaseModel):
|
||||||
created = DateTimeField(null=True)
|
created = DateTimeField(null=True)
|
||||||
comment = TextField(null=True)
|
comment = TextField(null=True)
|
||||||
command = 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):
|
class Image(BaseModel):
|
||||||
|
@ -249,7 +276,7 @@ class RepositoryBuild(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class QueueItem(BaseModel):
|
class QueueItem(BaseModel):
|
||||||
queue_name = CharField(index=True)
|
queue_name = CharField(index=True, max_length=1024)
|
||||||
body = TextField()
|
body = TextField()
|
||||||
available_after = DateTimeField(default=datetime.now, index=True)
|
available_after = DateTimeField(default=datetime.now, index=True)
|
||||||
available = BooleanField(default=True, index=True)
|
available = BooleanField(default=True, index=True)
|
||||||
|
|
76
data/migrations/env.py
Normal file
76
data/migrations/env.py
Normal file
|
@ -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()
|
||||||
|
|
22
data/migrations/script.py.mako
Normal file
22
data/migrations/script.py.mako
Normal file
|
@ -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"}
|
|
@ -4,14 +4,14 @@ import datetime
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
from data.database import *
|
from data.database import *
|
||||||
from util.validation import *
|
from util.validation import *
|
||||||
from util.names import format_robot_username
|
from util.names import format_robot_username
|
||||||
|
|
||||||
|
from app import storage as store
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
store = app.config['STORAGE']
|
|
||||||
transaction_factory = app.config['DB_TRANSACTION_FACTORY']
|
transaction_factory = app.config['DB_TRANSACTION_FACTORY']
|
||||||
|
|
||||||
class DataModelException(Exception):
|
class DataModelException(Exception):
|
||||||
|
@ -817,7 +817,7 @@ def get_repository(namespace_name, repository_name):
|
||||||
|
|
||||||
def get_repo_image(namespace_name, repository_name, image_id):
|
def get_repo_image(namespace_name, repository_name, image_id):
|
||||||
query = (Image
|
query = (Image
|
||||||
.select()
|
.select(Image, ImageStorage)
|
||||||
.join(Repository)
|
.join(Repository)
|
||||||
.switch(Image)
|
.switch(Image)
|
||||||
.join(ImageStorage, JOIN_LEFT_OUTER)
|
.join(ImageStorage, JOIN_LEFT_OUTER)
|
||||||
|
@ -1489,7 +1489,8 @@ def get_pull_credentials(robotname):
|
||||||
return {
|
return {
|
||||||
'username': robot.username,
|
'username': robot.username,
|
||||||
'password': login_info.service_ident,
|
'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
|
return webhook
|
||||||
|
|
||||||
|
|
||||||
def list_logs(user_or_organization_name, start_time, end_time, performer=None,
|
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
||||||
repository=None):
|
|
||||||
joined = LogEntry.select().join(User)
|
joined = LogEntry.select().join(User)
|
||||||
if repository:
|
if repository:
|
||||||
joined = joined.where(LogEntry.repository == 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:
|
if performer:
|
||||||
joined = joined.where(LogEntry.performer == performer)
|
joined = joined.where(LogEntry.performer == performer)
|
||||||
|
|
||||||
|
if namespace:
|
||||||
|
joined = joined.where(User.username == namespace)
|
||||||
|
|
||||||
return joined.where(
|
return joined.where(
|
||||||
User.username == user_or_organization_name,
|
|
||||||
LogEntry.datetime >= start_time,
|
LogEntry.datetime >= start_time,
|
||||||
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc())
|
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)
|
kind_ref = NotificationKind.get(name=kind)
|
||||||
Notification.delete().where(Notification.target == target,
|
Notification.delete().where(Notification.target == target,
|
||||||
Notification.kind == kind_ref).execute()
|
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
|
||||||
|
|
76
data/model/sqlalchemybridge.py
Normal file
76
data/model/sqlalchemybridge.py
Normal file
|
@ -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
|
104
data/plans.py
104
data/plans.py
|
@ -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
|
|
|
@ -8,17 +8,26 @@ transaction_factory = app.config['DB_TRANSACTION_FACTORY']
|
||||||
|
|
||||||
|
|
||||||
class WorkQueue(object):
|
class WorkQueue(object):
|
||||||
def __init__(self, queue_name):
|
def __init__(self, queue_name, canonical_name_match_list=None):
|
||||||
self.queue_name = queue_name
|
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,
|
Put an item, if it shouldn't be processed for some number of seconds,
|
||||||
specify that amount as available_after.
|
specify that amount as available_after.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'queue_name': self.queue_name,
|
'queue_name': self._canonical_name([self.queue_name] + canonical_name_list),
|
||||||
'body': message,
|
'body': message,
|
||||||
'retries_remaining': retries_remaining,
|
'retries_remaining': retries_remaining,
|
||||||
}
|
}
|
||||||
|
@ -35,16 +44,25 @@ class WorkQueue(object):
|
||||||
minutes.
|
minutes.
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
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):
|
with transaction_factory(db):
|
||||||
avail = QueueItem.select().where(QueueItem.queue_name == self.queue_name,
|
running = (QueueItem
|
||||||
QueueItem.available_after <= now,
|
.select(QueueItem.queue_name)
|
||||||
available_or_expired,
|
.where(QueueItem.available == False,
|
||||||
QueueItem.retries_remaining > 0)
|
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:
|
if found:
|
||||||
item = found[0]
|
item = found[0]
|
||||||
|
@ -57,16 +75,24 @@ class WorkQueue(object):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def complete(self, completed_item):
|
@staticmethod
|
||||||
|
def complete(completed_item):
|
||||||
completed_item.delete_instance()
|
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)
|
retry_date = datetime.now() + timedelta(seconds=retry_after)
|
||||||
incomplete_item.available_after = retry_date
|
incomplete_item.available_after = retry_date
|
||||||
incomplete_item.available = True
|
incomplete_item.available = True
|
||||||
incomplete_item.save()
|
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')
|
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'])
|
||||||
webhook_queue = WorkQueue('webhook')
|
dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME'])
|
||||||
|
webhook_queue = WorkQueue(app.config['WEBHOOK_QUEUE_NAME'])
|
||||||
|
|
|
@ -1,25 +1,43 @@
|
||||||
import boto
|
import boto
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import hashlib
|
||||||
|
import magic
|
||||||
|
|
||||||
from boto.s3.key import Key
|
from boto.s3.key import Key
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from flask import url_for, request, send_file, make_response, abort
|
||||||
|
from flask.views import View
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class S3FileWriteException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserRequestFiles(object):
|
class S3Userfiles(object):
|
||||||
def __init__(self, s3_access_key, s3_secret_key, bucket_name):
|
def __init__(self, path, s3_access_key, s3_secret_key, bucket_name):
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
self._bucket_name = bucket_name
|
self._bucket_name = bucket_name
|
||||||
self._access_key = s3_access_key
|
self._access_key = s3_access_key
|
||||||
self._secret_key = s3_secret_key
|
self._secret_key = s3_secret_key
|
||||||
self._prefix = 'userfiles'
|
self._prefix = path
|
||||||
self._s3_conn = None
|
self._s3_conn = None
|
||||||
self._bucket = None
|
self._bucket = None
|
||||||
|
|
||||||
|
@ -70,3 +88,139 @@ class UserRequestFiles(object):
|
||||||
full_key = os.path.join(self._prefix, file_id)
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
k = self._bucket.lookup(full_key)
|
k = self._bucket.lookup(full_key)
|
||||||
return k.etag[1:-1][:7]
|
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/<file_id>',
|
||||||
|
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)
|
||||||
|
|
|
@ -85,11 +85,32 @@ def handle_api_error(error):
|
||||||
|
|
||||||
def resource(*urls, **kwargs):
|
def resource(*urls, **kwargs):
|
||||||
def wrapper(api_resource):
|
def wrapper(api_resource):
|
||||||
|
if not api_resource:
|
||||||
|
return None
|
||||||
|
|
||||||
api.add_resource(api_resource, *urls, **kwargs)
|
api.add_resource(api_resource, *urls, **kwargs)
|
||||||
return api_resource
|
return api_resource
|
||||||
return wrapper
|
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):
|
def truthy_bool(param):
|
||||||
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
|
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 add_method_metadata(name, value):
|
||||||
def modifier(func):
|
def modifier(func):
|
||||||
|
if func is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if '__api_metadata' not in dir(func):
|
if '__api_metadata' not in dir(func):
|
||||||
func.__api_metadata = {}
|
func.__api_metadata = {}
|
||||||
func.__api_metadata[name] = value
|
func.__api_metadata[name] = value
|
||||||
|
@ -111,11 +135,15 @@ def add_method_metadata(name, value):
|
||||||
|
|
||||||
|
|
||||||
def method_metadata(func, name):
|
def method_metadata(func, name):
|
||||||
|
if func is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if '__api_metadata' in dir(func):
|
if '__api_metadata' in dir(func):
|
||||||
return func.__api_metadata.get(name, None)
|
return func.__api_metadata.get(name, None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
nickname = partial(add_method_metadata, 'nickname')
|
nickname = partial(add_method_metadata, 'nickname')
|
||||||
related_user_resource = partial(add_method_metadata, 'related_user_resource')
|
related_user_resource = partial(add_method_metadata, 'related_user_resource')
|
||||||
internal_only = add_method_metadata('internal', True)
|
internal_only = add_method_metadata('internal', True)
|
||||||
|
@ -274,6 +302,7 @@ import endpoints.api.repository
|
||||||
import endpoints.api.repotoken
|
import endpoints.api.repotoken
|
||||||
import endpoints.api.robot
|
import endpoints.api.robot
|
||||||
import endpoints.api.search
|
import endpoints.api.search
|
||||||
|
import endpoints.api.superuser
|
||||||
import endpoints.api.tag
|
import endpoints.api.tag
|
||||||
import endpoints.api.team
|
import endpoints.api.team
|
||||||
import endpoints.api.trigger
|
import endpoints.api.trigger
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import stripe
|
import stripe
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
from app import billing
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
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 endpoints.api.subscribe import subscribe, subscription_view
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data import model
|
from data import model
|
||||||
from data.plans import PLANS
|
from data.billing import PLANS
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
def carderror_response(e):
|
def carderror_response(e):
|
||||||
return {'carderror': e.message}, 402
|
return {'carderror': e.message}, 402
|
||||||
|
@ -22,7 +23,7 @@ def get_card(user):
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
if cus and cus.default_card:
|
if cus and cus.default_card:
|
||||||
# Find the default card.
|
# Find the default card.
|
||||||
default_card = None
|
default_card = None
|
||||||
|
@ -43,7 +44,7 @@ def get_card(user):
|
||||||
|
|
||||||
def set_card(user, token):
|
def set_card(user, token):
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
if cus:
|
if cus:
|
||||||
try:
|
try:
|
||||||
cus.card = token
|
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
|
'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 {
|
return {
|
||||||
'invoices': [invoice_view(i) for i in invoices.data]
|
'invoices': [invoice_view(i) for i in invoices.data]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/plans/')
|
@resource('/v1/plans/')
|
||||||
|
@show_if(features.BILLING)
|
||||||
class ListPlans(ApiResource):
|
class ListPlans(ApiResource):
|
||||||
""" Resource for listing the available plans. """
|
""" Resource for listing the available plans. """
|
||||||
@nickname('listPlans')
|
@nickname('listPlans')
|
||||||
|
@ -91,6 +93,7 @@ class ListPlans(ApiResource):
|
||||||
|
|
||||||
@resource('/v1/user/card')
|
@resource('/v1/user/card')
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.BILLING)
|
||||||
class UserCard(ApiResource):
|
class UserCard(ApiResource):
|
||||||
""" Resource for managing a user's credit card. """
|
""" Resource for managing a user's credit card. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -132,6 +135,7 @@ class UserCard(ApiResource):
|
||||||
@resource('/v1/organization/<orgname>/card')
|
@resource('/v1/organization/<orgname>/card')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(UserCard)
|
@related_user_resource(UserCard)
|
||||||
|
@show_if(features.BILLING)
|
||||||
class OrganizationCard(ApiResource):
|
class OrganizationCard(ApiResource):
|
||||||
""" Resource for managing an organization's credit card. """
|
""" Resource for managing an organization's credit card. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
|
||||||
|
|
||||||
@resource('/v1/user/plan')
|
@resource('/v1/user/plan')
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.BILLING)
|
||||||
class UserPlan(ApiResource):
|
class UserPlan(ApiResource):
|
||||||
""" Resource for managing a user's subscription. """
|
""" Resource for managing a user's subscription. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -216,16 +221,19 @@ class UserPlan(ApiResource):
|
||||||
@nickname('getUserSubscription')
|
@nickname('getUserSubscription')
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Fetch any existing subscription for the user. """
|
""" Fetch any existing subscription for the user. """
|
||||||
|
cus = None
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
return subscription_view(cus.subscription, private_repos)
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'hasSubscription': False,
|
||||||
|
'isExistingCustomer': cus is not None,
|
||||||
'plan': 'free',
|
'plan': 'free',
|
||||||
'usedPrivateRepos': private_repos,
|
'usedPrivateRepos': private_repos,
|
||||||
}
|
}
|
||||||
|
@ -234,6 +242,7 @@ class UserPlan(ApiResource):
|
||||||
@resource('/v1/organization/<orgname>/plan')
|
@resource('/v1/organization/<orgname>/plan')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(UserPlan)
|
@related_user_resource(UserPlan)
|
||||||
|
@show_if(features.BILLING)
|
||||||
class OrganizationPlan(ApiResource):
|
class OrganizationPlan(ApiResource):
|
||||||
""" Resource for managing a org's subscription. """
|
""" Resource for managing a org's subscription. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -274,17 +283,20 @@ class OrganizationPlan(ApiResource):
|
||||||
@nickname('getOrgSubscription')
|
@nickname('getOrgSubscription')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" Fetch any existing subscription for the org. """
|
""" Fetch any existing subscription for the org. """
|
||||||
|
cus = None
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
private_repos = model.get_private_repo_count(orgname)
|
private_repos = model.get_private_repo_count(orgname)
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
if organization.stripe_id:
|
if organization.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||||
|
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
return subscription_view(cus.subscription, private_repos)
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'hasSubscription': False,
|
||||||
|
'isExistingCustomer': cus is not None,
|
||||||
'plan': 'free',
|
'plan': 'free',
|
||||||
'usedPrivateRepos': private_repos,
|
'usedPrivateRepos': private_repos,
|
||||||
}
|
}
|
||||||
|
@ -294,6 +306,7 @@ class OrganizationPlan(ApiResource):
|
||||||
|
|
||||||
@resource('/v1/user/invoices')
|
@resource('/v1/user/invoices')
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.BILLING)
|
||||||
class UserInvoiceList(ApiResource):
|
class UserInvoiceList(ApiResource):
|
||||||
""" Resource for listing a user's invoices. """
|
""" Resource for listing a user's invoices. """
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
|
@ -310,6 +323,7 @@ class UserInvoiceList(ApiResource):
|
||||||
@resource('/v1/organization/<orgname>/invoices')
|
@resource('/v1/organization/<orgname>/invoices')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(UserInvoiceList)
|
@related_user_resource(UserInvoiceList)
|
||||||
|
@show_if(features.BILLING)
|
||||||
class OrgnaizationInvoiceList(ApiResource):
|
class OrgnaizationInvoiceList(ApiResource):
|
||||||
""" Resource for listing an orgnaization's invoices. """
|
""" Resource for listing an orgnaization's invoices. """
|
||||||
@nickname('listOrgInvoices')
|
@nickname('listOrgInvoices')
|
||||||
|
@ -323,4 +337,4 @@ class OrgnaizationInvoiceList(ApiResource):
|
||||||
|
|
||||||
return get_invoices(organization.stripe_id)
|
return get_invoices(organization.stripe_id)
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
|
@ -3,7 +3,7 @@ import json
|
||||||
|
|
||||||
from flask import request
|
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,
|
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||||
require_repo_read, require_repo_write, validate_json_request,
|
require_repo_read, require_repo_write, validate_json_request,
|
||||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
|
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
|
||||||
|
@ -17,7 +17,6 @@ from util.names import parse_robot_username
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
user_files = app.config['USERFILES']
|
|
||||||
build_logs = app.config['BUILDLOGS']
|
build_logs = app.config['BUILDLOGS']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,12 @@ TYPE_CONVERTER = {
|
||||||
int: 'integer',
|
int: 'integer',
|
||||||
}
|
}
|
||||||
|
|
||||||
URL_SCHEME = app.config['URL_SCHEME']
|
PREFERRED_URL_SCHEME = app.config['PREFERRED_URL_SCHEME']
|
||||||
URL_HOST = app.config['URL_HOST']
|
SERVER_HOSTNAME = app.config['SERVER_HOSTNAME']
|
||||||
|
|
||||||
|
|
||||||
def fully_qualified_name(method_view_class):
|
def fully_qualified_name(method_view_class):
|
||||||
inst = method_view_class()
|
return '%s.%s' % (method_view_class.__module__, method_view_class.__name__)
|
||||||
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def swagger_route_data(include_internal=False, compact=False):
|
def swagger_route_data(include_internal=False, compact=False):
|
||||||
|
@ -143,7 +142,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
swagger_data = {
|
swagger_data = {
|
||||||
'apiVersion': 'v1',
|
'apiVersion': 'v1',
|
||||||
'swaggerVersion': '1.2',
|
'swaggerVersion': '1.2',
|
||||||
'basePath': '%s://%s' % (URL_SCHEME, URL_HOST),
|
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
|
||||||
'resourcePath': '/',
|
'resourcePath': '/',
|
||||||
'info': {
|
'info': {
|
||||||
'title': 'Quay.io API',
|
'title': 'Quay.io API',
|
||||||
|
@ -160,7 +159,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
"implicit": {
|
"implicit": {
|
||||||
"tokenName": "access_token",
|
"tokenName": "access_token",
|
||||||
"loginEndpoint": {
|
"loginEndpoint": {
|
||||||
"url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST),
|
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,16 +2,13 @@ import json
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from app import app
|
from app import storage as store
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
||||||
format_date, NotFound)
|
format_date, NotFound)
|
||||||
from data import model
|
from data import model
|
||||||
from util.cache import cache_control_flask_restful
|
from util.cache import cache_control_flask_restful
|
||||||
|
|
||||||
|
|
||||||
store = app.config['STORAGE']
|
|
||||||
|
|
||||||
|
|
||||||
def image_view(image):
|
def image_view(image):
|
||||||
extended_props = image
|
extended_props = image
|
||||||
if image.storage and image.storage.id:
|
if image.storage and image.storage.id:
|
||||||
|
|
|
@ -29,8 +29,7 @@ def log_view(log):
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
def get_logs(namespace, start_time, end_time, performer_name=None,
|
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
|
||||||
repository=None):
|
|
||||||
performer = None
|
performer = None
|
||||||
if performer_name:
|
if performer_name:
|
||||||
performer = model.get_user(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:
|
if not end_time:
|
||||||
end_time = datetime.today()
|
end_time = datetime.today()
|
||||||
|
|
||||||
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
|
logs = model.list_logs(start_time, end_time, performer=performer, repository=repository,
|
||||||
repository=repository)
|
namespace=namespace)
|
||||||
return {
|
return {
|
||||||
'start_time': format_date(start_time),
|
'start_time': format_date(start_time),
|
||||||
'end_time': format_date(end_time),
|
'end_time': format_date(end_time),
|
||||||
|
@ -80,7 +79,7 @@ class RepositoryLogs(RepositoryParamResource):
|
||||||
|
|
||||||
start_time = args['starttime']
|
start_time = args['starttime']
|
||||||
end_time = args['endtime']
|
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')
|
@resource('/v1/user/logs')
|
||||||
|
@ -100,7 +99,7 @@ class UserLogs(ApiResource):
|
||||||
end_time = args['endtime']
|
end_time = args['endtime']
|
||||||
|
|
||||||
user = get_authenticated_user()
|
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/<orgname>/logs')
|
@resource('/v1/organization/<orgname>/logs')
|
||||||
|
@ -121,6 +120,6 @@ class OrgLogs(ApiResource):
|
||||||
start_time = args['starttime']
|
start_time = args['starttime']
|
||||||
end_time = args['endtime']
|
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()
|
raise Unauthorized()
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
|
from app import billing as stripe
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
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.team import team_view
|
||||||
from endpoints.api.user import User, PrivateRepositories
|
from endpoints.api.user import User, PrivateRepositories
|
||||||
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
||||||
CreateRepositoryPermission)
|
CreateRepositoryPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data import model
|
from data import model
|
||||||
from data.plans import get_plan
|
from data.billing import get_plan
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -163,6 +165,7 @@ class Organization(ApiResource):
|
||||||
@resource('/v1/organization/<orgname>/private')
|
@resource('/v1/organization/<orgname>/private')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(PrivateRepositories)
|
@related_user_resource(PrivateRepositories)
|
||||||
|
@show_if(features.BILLING)
|
||||||
class OrgPrivateRepositories(ApiResource):
|
class OrgPrivateRepositories(ApiResource):
|
||||||
""" Custom verb to compute whether additional private repositories are available. """
|
""" Custom verb to compute whether additional private repositories are available. """
|
||||||
@nickname('getOrganizationPrivateAllowed')
|
@nickname('getOrganizationPrivateAllowed')
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
import stripe
|
||||||
|
|
||||||
|
from app import billing
|
||||||
from endpoints.api import request_error, log_action, NotFound
|
from endpoints.api import request_error, log_action, NotFound
|
||||||
from endpoints.common import check_repository_usage
|
from endpoints.common import check_repository_usage
|
||||||
from data import model
|
from data import model
|
||||||
from data.plans import PLANS
|
from data.billing import PLANS
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -15,15 +17,24 @@ def carderror_response(exc):
|
||||||
|
|
||||||
|
|
||||||
def subscription_view(stripe_subscription, used_repos):
|
def subscription_view(stripe_subscription, used_repos):
|
||||||
return {
|
view = {
|
||||||
|
'hasSubscription': True,
|
||||||
|
'isExistingCustomer': True,
|
||||||
'currentPeriodStart': stripe_subscription.current_period_start,
|
'currentPeriodStart': stripe_subscription.current_period_start,
|
||||||
'currentPeriodEnd': stripe_subscription.current_period_end,
|
'currentPeriodEnd': stripe_subscription.current_period_end,
|
||||||
'plan': stripe_subscription.plan.id,
|
'plan': stripe_subscription.plan.id,
|
||||||
'usedPrivateRepos': used_repos,
|
'usedPrivateRepos': used_repos,
|
||||||
|
'trialStart': stripe_subscription.trial_start,
|
||||||
|
'trialEnd': stripe_subscription.trial_end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
|
||||||
|
|
||||||
def subscribe(user, plan, token, require_business_plan):
|
def subscribe(user, plan, token, require_business_plan):
|
||||||
|
if not features.BILLING:
|
||||||
|
return
|
||||||
|
|
||||||
plan_found = None
|
plan_found = None
|
||||||
for plan_obj in PLANS:
|
for plan_obj in PLANS:
|
||||||
if plan_obj['stripeId'] == plan:
|
if plan_obj['stripeId'] == plan:
|
||||||
|
@ -56,7 +67,7 @@ def subscribe(user, plan, token, require_business_plan):
|
||||||
card = token
|
card = token
|
||||||
|
|
||||||
try:
|
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.stripe_id = cus.id
|
||||||
user.save()
|
user.save()
|
||||||
check_repository_usage(user, plan_found)
|
check_repository_usage(user, plan_found)
|
||||||
|
@ -69,7 +80,7 @@ def subscribe(user, plan, token, require_business_plan):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Change the plan
|
# Change the plan
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
|
||||||
if plan_found['price'] == 0:
|
if plan_found['price'] == 0:
|
||||||
if cus.subscription is not None:
|
if cus.subscription is not None:
|
||||||
|
|
160
endpoints/api/superuser.py
Normal file
160
endpoints/api/superuser.py
Normal file
|
@ -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/<username>')
|
||||||
|
@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)
|
|
@ -1,5 +1,7 @@
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_admin,
|
from flask import request
|
||||||
RepositoryParamResource, log_action, NotFound)
|
|
||||||
|
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 endpoints.api.image import image_view
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -8,8 +10,54 @@ from auth.auth_context import get_authenticated_user
|
||||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||||
class RepositoryTag(RepositoryParamResource):
|
class RepositoryTag(RepositoryParamResource):
|
||||||
""" Resource for managing repository tags. """
|
""" 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')
|
@nickname('deleteFullTag')
|
||||||
def delete(self, namespace, repository, tag):
|
def delete(self, namespace, repository, tag):
|
||||||
""" Delete the specified repository tag. """
|
""" Delete the specified repository tag. """
|
||||||
|
|
|
@ -205,9 +205,8 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
trigger.repository.name)
|
trigger.repository.name)
|
||||||
path = url_for('webhooks.build_trigger_webhook',
|
path = url_for('webhooks.build_trigger_webhook',
|
||||||
repository=repository_path, trigger_uuid=trigger.uuid)
|
repository=repository_path, trigger_uuid=trigger.uuid)
|
||||||
authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token',
|
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code,
|
||||||
token.code, app.config['URL_HOST'],
|
app.config['SERVER_HOSTNAME'], path)
|
||||||
path)
|
|
||||||
|
|
||||||
final_config = handler.activate(trigger.uuid, authed_url,
|
final_config = handler.activate(trigger.uuid, authed_url,
|
||||||
trigger.auth_token, new_config_dict)
|
trigger.auth_token, new_config_dict)
|
||||||
|
@ -294,7 +293,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check to see if the base image lives in Quay.
|
# 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):
|
if not base_image.startswith(quay_registry_prefix):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask.ext.login import logout_user
|
from flask.ext.login import logout_user
|
||||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
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,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin,
|
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.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from data import model
|
from data import model
|
||||||
from data.plans import get_plan
|
from data.billing import get_plan
|
||||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||||
UserAdminPermission, UserReadPermission)
|
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
from util.email import (send_confirmation_email, send_recovery_email,
|
from util.email import (send_confirmation_email, send_recovery_email,
|
||||||
send_change_email)
|
send_change_email)
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -65,6 +65,11 @@ def user_view(user):
|
||||||
'preferred_namespace': not (user.stripe_id is None),
|
'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
|
return user_response
|
||||||
|
|
||||||
|
|
||||||
|
@ -193,6 +198,7 @@ class User(ApiResource):
|
||||||
|
|
||||||
@resource('/v1/user/private')
|
@resource('/v1/user/private')
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.BILLING)
|
||||||
class PrivateRepositories(ApiResource):
|
class PrivateRepositories(ApiResource):
|
||||||
""" Operations dealing with the available count of private repositories. """
|
""" Operations dealing with the available count of private repositories. """
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
|
@ -248,8 +254,7 @@ class ConvertToOrganization(ApiResource):
|
||||||
'description': 'Information required to convert a user to an organization.',
|
'description': 'Information required to convert a user to an organization.',
|
||||||
'required': [
|
'required': [
|
||||||
'adminUser',
|
'adminUser',
|
||||||
'adminPassword',
|
'adminPassword'
|
||||||
'plan',
|
|
||||||
],
|
],
|
||||||
'properties': {
|
'properties': {
|
||||||
'adminUser': {
|
'adminUser': {
|
||||||
|
@ -262,7 +267,7 @@ class ConvertToOrganization(ApiResource):
|
||||||
},
|
},
|
||||||
'plan': {
|
'plan': {
|
||||||
'type': 'string',
|
'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')
|
message='The admin user credentials are not valid')
|
||||||
|
|
||||||
# Subscribe the organization to the new plan.
|
# Subscribe the organization to the new plan.
|
||||||
plan = convert_data['plan']
|
if features.BILLING:
|
||||||
subscribe(user, plan, None, True) # Require business plans
|
plan = convert_data.get('plan', 'free')
|
||||||
|
subscribe(user, plan, None, True) # Require business plans
|
||||||
|
|
||||||
# Convert the user to an organization.
|
# Convert the user to an organization.
|
||||||
model.convert_user_to_organization(user, model.get_user(admin_username))
|
model.convert_user_to_organization(user, model.get_user(admin_username))
|
||||||
|
|
|
@ -3,14 +3,15 @@ import logging
|
||||||
from flask import request, redirect, url_for, Blueprint
|
from flask import request, redirect, url_for, Blueprint
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from endpoints.common import render_page_template, common_login
|
from endpoints.common import render_page_template, common_login, route_show_if
|
||||||
from app import app, mixpanel
|
from app import app, analytics
|
||||||
from data import model
|
from data import model
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from auth.permissions import AdministerRepositoryPermission
|
from auth.permissions import AdministerRepositoryPermission
|
||||||
from auth.auth import require_session_login
|
from auth.auth import require_session_login
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -20,11 +21,11 @@ client = app.config['HTTPCLIENT']
|
||||||
callback = Blueprint('callback', __name__)
|
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')
|
code = request.args.get('code')
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'],
|
||||||
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'],
|
||||||
'code': code,
|
'code': code,
|
||||||
}
|
}
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -48,6 +49,7 @@ def get_github_user(token):
|
||||||
|
|
||||||
|
|
||||||
@callback.route('/github/callback', methods=['GET'])
|
@callback.route('/github/callback', methods=['GET'])
|
||||||
|
@route_show_if(features.GITHUB_LOGIN)
|
||||||
def github_oauth_callback():
|
def github_oauth_callback():
|
||||||
error = request.args.get('error', None)
|
error = request.args.get('error', None)
|
||||||
if error:
|
if error:
|
||||||
|
@ -83,13 +85,13 @@ def github_oauth_callback():
|
||||||
to_login = model.create_federated_user(username, found_email, 'github',
|
to_login = model.create_federated_user(username, found_email, 'github',
|
||||||
github_id)
|
github_id)
|
||||||
|
|
||||||
# Success, tell mixpanel
|
# Success, tell analytics
|
||||||
mixpanel.track(to_login.username, 'register', {'service': 'github'})
|
analytics.track(to_login.username, 'register', {'service': 'github'})
|
||||||
|
|
||||||
state = request.args.get('state', None)
|
state = request.args.get('state', None)
|
||||||
if state:
|
if state:
|
||||||
logger.debug('Aliasing with state: %s' % state)
|
logger.debug('Aliasing with state: %s' % state)
|
||||||
mixpanel.alias(to_login.username, state)
|
analytics.alias(to_login.username, state)
|
||||||
|
|
||||||
except model.DataModelException, ex:
|
except model.DataModelException, ex:
|
||||||
return render_page_template('githuberror.html', error_message=ex.message)
|
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'])
|
@callback.route('/github/callback/attach', methods=['GET'])
|
||||||
|
@route_show_if(features.GITHUB_LOGIN)
|
||||||
@require_session_login
|
@require_session_login
|
||||||
def github_oauth_attach():
|
def github_oauth_attach():
|
||||||
token = exchange_github_code_for_token(request.args.get('code'))
|
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):
|
def attach_github_build_trigger(namespace, repository):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
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)
|
repo = model.get_repository(namespace, repository)
|
||||||
if not repo:
|
if not repo:
|
||||||
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import urlparse
|
||||||
import json
|
import json
|
||||||
import string
|
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.login import login_user, UserMixin
|
||||||
from flask.ext.principal import identity_changed
|
from flask.ext.principal import identity_changed
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
@ -15,7 +15,10 @@ from auth.permissions import QuayDeferredPermissionUser
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from endpoints.api.discovery import swagger_route_data
|
from endpoints.api.discovery import swagger_route_data
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
from functools import wraps
|
||||||
|
from config import getFrontendVisibleConfig
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -27,6 +30,29 @@ class RepoPathConverter(BaseConverter):
|
||||||
|
|
||||||
app.url_map.converters['repopath'] = RepoPathConverter
|
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():
|
def get_route_data():
|
||||||
global route_data
|
global route_data
|
||||||
|
@ -89,9 +115,52 @@ def random_string():
|
||||||
random = SystemRandom()
|
random = SystemRandom()
|
||||||
return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)])
|
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):
|
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()),
|
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'
|
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -125,7 +194,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
dockerfile_id, build_name,
|
dockerfile_id, build_name,
|
||||||
trigger, pull_robot_name = pull_robot_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,
|
'build_uuid': build_request.uuid,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace,
|
||||||
'repository': repository.name,
|
'repository': repository.name,
|
||||||
|
|
|
@ -9,7 +9,7 @@ from collections import OrderedDict
|
||||||
from data import model
|
from data import model
|
||||||
from data.model import oauth
|
from data.model import oauth
|
||||||
from data.queue import webhook_queue
|
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 import process_auth
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -21,6 +21,7 @@ from util.http import abort
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
profile = logging.getLogger('application.profiler')
|
||||||
|
|
||||||
index = Blueprint('index', __name__)
|
index = Blueprint('index', __name__)
|
||||||
|
|
||||||
|
@ -112,9 +113,15 @@ def create_user():
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# New user case
|
# New user case
|
||||||
|
profile.debug('Creating user')
|
||||||
new_user = model.create_user(username, password, user_data['email'])
|
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)
|
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)
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||||
|
|
||||||
return make_response('Created', 201)
|
return make_response('Created', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,12 +156,12 @@ def update_user(username):
|
||||||
update_request = request.get_json()
|
update_request = request.get_json()
|
||||||
|
|
||||||
if 'password' in update_request:
|
if 'password' in update_request:
|
||||||
logger.debug('Updating user password.')
|
profile.debug('Updating user password')
|
||||||
model.change_password(get_authenticated_user(),
|
model.change_password(get_authenticated_user(),
|
||||||
update_request['password'])
|
update_request['password'])
|
||||||
|
|
||||||
if 'email' in update_request:
|
if 'email' in update_request:
|
||||||
logger.debug('Updating user email')
|
profile.debug('Updating user email')
|
||||||
model.update_email(get_authenticated_user(), update_request['email'])
|
model.update_email(get_authenticated_user(), update_request['email'])
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
@ -170,9 +177,13 @@ def update_user(username):
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers(role='write')
|
@generate_headers(role='write')
|
||||||
def create_repository(namespace, repository):
|
def create_repository(namespace, repository):
|
||||||
|
profile.debug('Parsing image descriptions')
|
||||||
image_descriptions = json.loads(request.data)
|
image_descriptions = json.loads(request.data)
|
||||||
|
|
||||||
|
profile.debug('Looking up repository')
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
|
|
||||||
|
profile.debug('Repository looked up')
|
||||||
if not repo and get_authenticated_user() is None:
|
if not repo and get_authenticated_user() is None:
|
||||||
logger.debug('Attempt to create new repository without user auth.')
|
logger.debug('Attempt to create new repository without user auth.')
|
||||||
abort(401,
|
abort(401,
|
||||||
|
@ -196,11 +207,11 @@ def create_repository(namespace, repository):
|
||||||
issue='no-create-permission',
|
issue='no-create-permission',
|
||||||
namespace=namespace)
|
namespace=namespace)
|
||||||
|
|
||||||
logger.debug('Creaing repository with owner: %s' %
|
profile.debug('Creaing repository with owner: %s', get_authenticated_user().username)
|
||||||
get_authenticated_user().username)
|
|
||||||
repo = model.create_repository(namespace, repository,
|
repo = model.create_repository(namespace, repository,
|
||||||
get_authenticated_user())
|
get_authenticated_user())
|
||||||
|
|
||||||
|
profile.debug('Determining added images')
|
||||||
added_images = OrderedDict([(desc['id'], desc)
|
added_images = OrderedDict([(desc['id'], desc)
|
||||||
for desc in image_descriptions])
|
for desc in image_descriptions])
|
||||||
new_repo_images = dict(added_images)
|
new_repo_images = dict(added_images)
|
||||||
|
@ -209,12 +220,15 @@ def create_repository(namespace, repository):
|
||||||
if existing.docker_image_id in new_repo_images:
|
if existing.docker_image_id in new_repo_images:
|
||||||
added_images.pop(existing.docker_image_id)
|
added_images.pop(existing.docker_image_id)
|
||||||
|
|
||||||
|
profile.debug('Creating/Linking necessary images')
|
||||||
username = get_authenticated_user() and get_authenticated_user().username
|
username = get_authenticated_user() and get_authenticated_user().username
|
||||||
translations = {}
|
translations = {}
|
||||||
for image_description in added_images.values():
|
for image_description in added_images.values():
|
||||||
model.find_create_or_link_image(image_description['id'], repo, username,
|
model.find_create_or_link_image(image_description['id'], repo, username,
|
||||||
translations)
|
translations)
|
||||||
|
|
||||||
|
|
||||||
|
profile.debug('Created images')
|
||||||
response = make_response('Created', 201)
|
response = make_response('Created', 201)
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
|
@ -227,7 +241,7 @@ def create_repository(namespace, repository):
|
||||||
}
|
}
|
||||||
|
|
||||||
if get_validated_oauth_token():
|
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()
|
oauth_token = get_validated_oauth_token()
|
||||||
metadata['oauth_token_id'] = oauth_token.id
|
metadata['oauth_token_id'] = oauth_token.id
|
||||||
|
@ -236,7 +250,7 @@ def create_repository(namespace, repository):
|
||||||
elif get_authenticated_user():
|
elif get_authenticated_user():
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
|
|
||||||
mixpanel.track(username, 'push_repo', extra_params)
|
analytics.track(username, 'push_repo', extra_params)
|
||||||
metadata['username'] = username
|
metadata['username'] = username
|
||||||
|
|
||||||
# Mark that the user has started pushing the repo.
|
# 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)
|
event.publish_event_data('docker-cli', user_data)
|
||||||
|
|
||||||
elif get_validated_token():
|
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'] = get_validated_token().friendly_name
|
||||||
metadata['token_code'] = get_validated_token().code
|
metadata['token_code'] = get_validated_token().code
|
||||||
|
|
||||||
|
@ -268,21 +282,23 @@ def update_images(namespace, repository):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
profile.debug('Looking up repository')
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
if not repo:
|
if not repo:
|
||||||
# Make sure the repo actually exists.
|
# Make sure the repo actually exists.
|
||||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||||
|
|
||||||
|
profile.debug('Parsing image data')
|
||||||
image_with_checksums = json.loads(request.data)
|
image_with_checksums = json.loads(request.data)
|
||||||
|
|
||||||
updated_tags = {}
|
updated_tags = {}
|
||||||
for image in image_with_checksums:
|
for image in image_with_checksums:
|
||||||
logger.debug('Setting checksum for image id: %s to %s' %
|
profile.debug('Setting checksum for image id: %s to %s', image['id'], image['checksum'])
|
||||||
(image['id'], image['checksum']))
|
|
||||||
updated_tags[image['Tag']] = image['id']
|
updated_tags[image['Tag']] = image['id']
|
||||||
model.set_image_checksum(image['id'], repo, image['checksum'])
|
model.set_image_checksum(image['id'], repo, image['checksum'])
|
||||||
|
|
||||||
if get_authenticated_user():
|
if get_authenticated_user():
|
||||||
|
profile.debug('Publishing push event')
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
|
|
||||||
# Mark that the user has pushed the repo.
|
# 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 = app.config['USER_EVENTS'].get_event(username)
|
||||||
event.publish_event_data('docker-cli', user_data)
|
event.publish_event_data('docker-cli', user_data)
|
||||||
|
|
||||||
|
profile.debug('GCing repository')
|
||||||
num_removed = model.garbage_collect_repository(namespace, repository)
|
num_removed = model.garbage_collect_repository(namespace, repository)
|
||||||
|
|
||||||
# Generate a job for each webhook that has been added to this repo
|
# 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)
|
webhooks = model.list_webhooks(namespace, repository)
|
||||||
for webhook in webhooks:
|
for webhook in webhooks:
|
||||||
webhook_data = json.loads(webhook.parameters)
|
webhook_data = json.loads(webhook.parameters)
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
repo_string = '%s/%s' % (namespace, repository)
|
||||||
logger.debug('Creating webhook for repository \'%s\' for url \'%s\'' %
|
profile.debug('Creating webhook for repository \'%s\' for url \'%s\'',
|
||||||
(repo_string, webhook_data['url']))
|
repo_string, webhook_data['url'])
|
||||||
webhook_data['payload'] = {
|
webhook_data['payload'] = {
|
||||||
'repository': repo_string,
|
'repository': repo_string,
|
||||||
'namespace': namespace,
|
'namespace': namespace,
|
||||||
|
@ -315,7 +334,7 @@ def update_images(namespace, repository):
|
||||||
'pushed_image_count': len(image_with_checksums),
|
'pushed_image_count': len(image_with_checksums),
|
||||||
'pruned_image_count': num_removed,
|
'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)
|
return make_response('Updated', 204)
|
||||||
|
|
||||||
|
@ -330,14 +349,17 @@ def get_repository_images(namespace, repository):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
# TODO invalidate token?
|
# TODO invalidate token?
|
||||||
|
profile.debug('Looking up public status of repository')
|
||||||
is_public = model.repository_is_public(namespace, repository)
|
is_public = model.repository_is_public(namespace, repository)
|
||||||
if permission.can() or is_public:
|
if permission.can() or is_public:
|
||||||
# We can't rely on permissions to tell us if a repo exists anymore
|
# 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)
|
repo = model.get_repository(namespace, repository)
|
||||||
if not repo:
|
if not repo:
|
||||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||||
|
|
||||||
all_images = []
|
all_images = []
|
||||||
|
profile.debug('Retrieving repository images')
|
||||||
for image in model.get_repository_images(namespace, repository):
|
for image in model.get_repository_images(namespace, repository):
|
||||||
new_image_view = {
|
new_image_view = {
|
||||||
'id': image.docker_image_id,
|
'id': image.docker_image_id,
|
||||||
|
@ -345,6 +367,7 @@ def get_repository_images(namespace, repository):
|
||||||
}
|
}
|
||||||
all_images.append(new_image_view)
|
all_images.append(new_image_view)
|
||||||
|
|
||||||
|
profile.debug('Building repository image response')
|
||||||
resp = make_response(json.dumps(all_images), 200)
|
resp = make_response(json.dumps(all_images), 200)
|
||||||
resp.mimetype = 'application/json'
|
resp.mimetype = 'application/json'
|
||||||
|
|
||||||
|
@ -353,6 +376,7 @@ def get_repository_images(namespace, repository):
|
||||||
'namespace': namespace,
|
'namespace': namespace,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profile.debug('Logging the pull to Mixpanel and the log system')
|
||||||
if get_validated_oauth_token():
|
if get_validated_oauth_token():
|
||||||
oauth_token = get_validated_oauth_token()
|
oauth_token = get_validated_oauth_token()
|
||||||
metadata['oauth_token_id'] = oauth_token.id
|
metadata['oauth_token_id'] = oauth_token.id
|
||||||
|
@ -374,7 +398,7 @@ def get_repository_images(namespace, repository):
|
||||||
'repository': '%s/%s' % (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,
|
model.log_action('pull_repo', namespace,
|
||||||
performer=get_authenticated_user(),
|
performer=get_authenticated_user(),
|
||||||
ip=request.remote_addr, metadata=metadata,
|
ip=request.remote_addr, metadata=metadata,
|
||||||
|
@ -408,4 +432,5 @@ def get_search():
|
||||||
def ping():
|
def ping():
|
||||||
response = make_response('true', 200)
|
response = make_response('true', 200)
|
||||||
response.headers['X-Docker-Registry-Version'] = '0.6.0'
|
response.headers['X-Docker-Registry-Version'] = '0.6.0'
|
||||||
|
response.headers['X-Docker-Registry-Standalone'] = '0'
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -9,7 +9,7 @@ from time import time
|
||||||
|
|
||||||
from data.queue import image_diff_queue
|
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 auth.auth import process_auth, extract_namespace_repo_from_session
|
||||||
from util import checksums, changes
|
from util import checksums, changes
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
|
@ -17,11 +17,11 @@ from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission)
|
ModifyRepositoryPermission)
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
registry = Blueprint('registry', __name__)
|
registry = Blueprint('registry', __name__)
|
||||||
|
|
||||||
store = app.config['STORAGE']
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
profile = logging.getLogger('application.profiler')
|
||||||
|
|
||||||
class SocketReader(object):
|
class SocketReader(object):
|
||||||
def __init__(self, fp):
|
def __init__(self, fp):
|
||||||
|
@ -40,16 +40,35 @@ class SocketReader(object):
|
||||||
return buf
|
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):
|
def require_completion(f):
|
||||||
"""This make sure that the image push correctly finished."""
|
"""This make sure that the image push correctly finished."""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(namespace, repository, *args, **kwargs):
|
def wrapper(namespace, repository, *args, **kwargs):
|
||||||
image_id = kwargs['image_id']
|
image_id = kwargs['image_id']
|
||||||
repo_image = model.get_repo_image(namespace, repository, 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 image_is_uploading(namespace, repository, image_id, repo_image):
|
||||||
|
|
||||||
if store.exists(store.image_mark_path(namespace, repository, image_id,
|
|
||||||
uuid)):
|
|
||||||
abort(400, 'Image %(image_id)s is being uploaded, retry later',
|
abort(400, 'Image %(image_id)s is being uploaded, retry later',
|
||||||
issue='upload-in-progress', image_id=kwargs['image_id'])
|
issue='upload-in-progress', image_id=kwargs['image_id'])
|
||||||
|
|
||||||
|
@ -88,17 +107,28 @@ def set_cache_headers(f):
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_layer(namespace, repository, image_id, headers):
|
def get_image_layer(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
if permission.can() or model.repository_is_public(namespace, repository):
|
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)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
|
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
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)
|
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)
|
direct_download_url = store.get_direct_download_url(path)
|
||||||
|
|
||||||
if direct_download_url:
|
if direct_download_url:
|
||||||
|
profile.debug('Returning direct download URL')
|
||||||
return redirect(direct_download_url)
|
return redirect(direct_download_url)
|
||||||
try:
|
try:
|
||||||
|
profile.debug('Streaming layer data')
|
||||||
return Response(store.stream_read(path), headers=headers)
|
return Response(store.stream_read(path), headers=headers)
|
||||||
except IOError:
|
except IOError:
|
||||||
|
profile.debug('Image not found')
|
||||||
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||||
image_id=image_id)
|
image_id=image_id)
|
||||||
|
|
||||||
|
@ -109,25 +139,32 @@ def get_image_layer(namespace, repository, image_id, headers):
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
def put_image_layer(namespace, repository, image_id):
|
def put_image_layer(namespace, repository, image_id):
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if not permission.can():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
profile.debug('Retrieving image')
|
||||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
|
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||||
try:
|
try:
|
||||||
|
profile.debug('Retrieving image data')
|
||||||
json_data = store.get_content(store.image_json_path(namespace, repository,
|
json_data = store.get_content(store.image_json_path(namespace, repository,
|
||||||
image_id, uuid))
|
image_id, uuid))
|
||||||
except IOError:
|
except IOError:
|
||||||
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||||
image_id=image_id)
|
image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Retrieving image path info')
|
||||||
layer_path = store.image_layer_path(namespace, repository, image_id, uuid)
|
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)
|
abort(409, 'Image already exists', issue='image-exists', image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Storing layer data')
|
||||||
|
|
||||||
input_stream = request.stream
|
input_stream = request.stream
|
||||||
if request.headers.get('transfer-encoding') == 'chunked':
|
if request.headers.get('transfer-encoding') == 'chunked':
|
||||||
# Careful, might work only with WSGI servers supporting 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)
|
issue='checksum-mismatch', image_id=image_id)
|
||||||
|
|
||||||
# Checksum is ok, we remove the marker
|
# 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
|
# The layer is ready for download, send a job to the work queue to
|
||||||
# process it.
|
# process it.
|
||||||
logger.debug('Queing diffs job for image: %s' % image_id)
|
profile.debug('Adding layer to diff queue')
|
||||||
image_diff_queue.put(json.dumps({
|
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||||
'namespace': namespace,
|
'namespace': namespace,
|
||||||
'repository': repository,
|
'repository': repository,
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
|
@ -192,6 +229,7 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
def put_image_checksum(namespace, repository, image_id):
|
def put_image_checksum(namespace, repository, image_id):
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if not permission.can():
|
||||||
abort(403)
|
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',
|
abort(400, 'Checksum not found in Cookie for image %(imaage_id)s',
|
||||||
issue='missing-checksum-cookie', image_id=image_id)
|
issue='missing-checksum-cookie', image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Looking up repo image')
|
||||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
|
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
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,
|
if not store.exists(store.image_json_path(namespace, repository, image_id,
|
||||||
uuid)):
|
uuid)):
|
||||||
abort(404, 'Image not found: %(image_id)s', issue='unknown-image', image_id=image_id)
|
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)
|
profile.debug('Marking image path')
|
||||||
if not store.exists(mark_path):
|
if not image_is_uploading(namespace, repository, image_id, repo_image):
|
||||||
abort(409, 'Cannot set checksum for image %(image_id)s',
|
abort(409, 'Cannot set checksum for image %(image_id)s',
|
||||||
issue='image-write-error', image_id=image_id)
|
issue='image-write-error', image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Storing image checksum')
|
||||||
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
||||||
if err:
|
if err:
|
||||||
abort(400, err)
|
abort(400, err)
|
||||||
|
@ -227,12 +270,12 @@ def put_image_checksum(namespace, repository, image_id):
|
||||||
issue='checksum-mismatch', image_id=image_id)
|
issue='checksum-mismatch', image_id=image_id)
|
||||||
|
|
||||||
# Checksum is ok, we remove the marker
|
# 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
|
# The layer is ready for download, send a job to the work queue to
|
||||||
# process it.
|
# process it.
|
||||||
logger.debug('Queing diffs job for image: %s' % image_id)
|
profile.debug('Adding layer to diff queue')
|
||||||
image_diff_queue.put(json.dumps({
|
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||||
'namespace': namespace,
|
'namespace': namespace,
|
||||||
'repository': repository,
|
'repository': repository,
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
|
@ -247,27 +290,31 @@ def put_image_checksum(namespace, repository, image_id):
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_json(namespace, repository, image_id, headers):
|
def get_image_json(namespace, repository, image_id, headers):
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not model.repository_is_public(namespace,
|
if not permission.can() and not model.repository_is_public(namespace,
|
||||||
repository):
|
repository):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
profile.debug('Looking up repo image')
|
||||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||||
|
|
||||||
|
profile.debug('Looking up repo layer data')
|
||||||
try:
|
try:
|
||||||
data = store.get_content(store.image_json_path(namespace, repository,
|
data = store.get_content(store.image_json_path(namespace, repository,
|
||||||
image_id, uuid))
|
image_id, uuid))
|
||||||
except IOError:
|
except IOError:
|
||||||
flask_abort(404)
|
flask_abort(404)
|
||||||
|
|
||||||
|
profile.debug('Looking up repo layer size')
|
||||||
try:
|
try:
|
||||||
size = store.get_size(store.image_layer_path(namespace, repository,
|
size = repo_image.image_size or repo_image.storage.image_size
|
||||||
image_id, uuid))
|
|
||||||
headers['X-Docker-Size'] = str(size)
|
headers['X-Docker-Size'] = str(size)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
profile.debug('Retrieving checksum')
|
||||||
checksum_path = store.image_checksum_path(namespace, repository, image_id,
|
checksum_path = store.image_checksum_path(namespace, repository, image_id,
|
||||||
uuid)
|
uuid)
|
||||||
if store.exists(checksum_path):
|
if store.exists(checksum_path):
|
||||||
|
@ -284,14 +331,17 @@ def get_image_json(namespace, repository, image_id, headers):
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
def get_image_ancestry(namespace, repository, image_id, headers):
|
def get_image_ancestry(namespace, repository, image_id, headers):
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not model.repository_is_public(namespace,
|
if not permission.can() and not model.repository_is_public(namespace,
|
||||||
repository):
|
repository):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
profile.debug('Looking up repo image')
|
||||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||||
|
|
||||||
|
profile.debug('Looking up image data')
|
||||||
try:
|
try:
|
||||||
data = store.get_content(store.image_ancestry_path(namespace, repository,
|
data = store.get_content(store.image_ancestry_path(namespace, repository,
|
||||||
image_id, uuid))
|
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',
|
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||||
image_id=image_id)
|
image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Converting to <-> from JSON')
|
||||||
response = make_response(json.dumps(json.loads(data)), 200)
|
response = make_response(json.dumps(json.loads(data)), 200)
|
||||||
response.headers.extend(headers)
|
response.headers.extend(headers)
|
||||||
|
|
||||||
|
profile.debug('Done')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -335,10 +388,12 @@ def store_checksum(namespace, repository, image_id, uuid, checksum):
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
def put_image_json(namespace, repository, image_id):
|
def put_image_json(namespace, repository, image_id):
|
||||||
|
profile.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if not permission.can():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
profile.debug('Parsing image JSON')
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.data)
|
data = json.loads(request.data)
|
||||||
except json.JSONDecodeError:
|
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',
|
abort(400, 'Missing key `id` in JSON for image: %(image_id)s',
|
||||||
issue='invalid-request', image_id=image_id)
|
issue='invalid-request', image_id=image_id)
|
||||||
|
|
||||||
|
profile.debug('Looking up repo image')
|
||||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
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')
|
checksum = request.headers.get('X-Docker-Checksum')
|
||||||
if checksum:
|
if checksum:
|
||||||
# Storing the checksum is optional at this stage
|
# Storing the checksum is optional at this stage
|
||||||
|
profile.debug('Storing image checksum')
|
||||||
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
||||||
if err:
|
if err:
|
||||||
abort(400, err, issue='write-error')
|
abort(400, err, issue='write-error')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# We cleanup any old checksum in case it's a retry after a fail
|
# 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,
|
store.remove(store.image_checksum_path(namespace, repository, image_id,
|
||||||
uuid))
|
uuid))
|
||||||
if image_id != data['id']:
|
if image_id != data['id']:
|
||||||
|
@ -374,19 +432,27 @@ def put_image_json(namespace, repository, image_id):
|
||||||
|
|
||||||
parent_image = None
|
parent_image = None
|
||||||
if parent_id:
|
if parent_id:
|
||||||
|
profile.debug('Looking up parent image')
|
||||||
parent_image = model.get_repo_image(namespace, repository, parent_id)
|
parent_image = model.get_repo_image(namespace, repository, parent_id)
|
||||||
|
|
||||||
parent_uuid = (parent_image and parent_image.storage and
|
parent_uuid = (parent_image and parent_image.storage and
|
||||||
parent_image.storage.uuid)
|
parent_image.storage.uuid)
|
||||||
|
|
||||||
|
if parent_id:
|
||||||
|
profile.debug('Looking up parent image data')
|
||||||
|
|
||||||
if (parent_id and not
|
if (parent_id and not
|
||||||
store.exists(store.image_json_path(namespace, repository, parent_id,
|
store.exists(store.image_json_path(namespace, repository, parent_id,
|
||||||
parent_uuid))):
|
parent_uuid))):
|
||||||
abort(400, 'Image %(image_id)s depends on non existing parent image %(parent_id)s',
|
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)
|
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)
|
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)
|
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
|
# 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
|
# save the metadata
|
||||||
command_list = data.get('container_config', {}).get('Cmd', None)
|
command_list = data.get('container_config', {}).get('Cmd', None)
|
||||||
command = json.dumps(command_list) if command_list else None
|
command = json.dumps(command_list) if command_list else None
|
||||||
|
|
||||||
|
profile.debug('Setting image metadata')
|
||||||
model.set_image_metadata(image_id, namespace, repository,
|
model.set_image_metadata(image_id, namespace, repository,
|
||||||
data.get('created'), data.get('comment'), command,
|
data.get('created'), data.get('comment'), command,
|
||||||
parent_image)
|
parent_image)
|
||||||
store.put_content(mark_path, 'true')
|
|
||||||
|
profile.debug('Putting json path')
|
||||||
store.put_content(json_path, request.data)
|
store.put_content(json_path, request.data)
|
||||||
|
|
||||||
|
profile.debug('Generating image ancestry')
|
||||||
generate_ancestry(namespace, repository, image_id, uuid, parent_id,
|
generate_ancestry(namespace, repository, image_id, uuid, parent_id,
|
||||||
parent_uuid)
|
parent_uuid)
|
||||||
|
|
||||||
|
profile.debug('Done')
|
||||||
return make_response('true', 200)
|
return make_response('true', 200)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,9 @@ import base64
|
||||||
from github import Github, UnknownObjectException, GithubException
|
from github import Github, UnknownObjectException, GithubException
|
||||||
from tempfile import SpooledTemporaryFile
|
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']
|
client = app.config['HTTPCLIENT']
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,6 +20,10 @@ TARBALL_MIME = 'application/gzip'
|
||||||
CHUNK_SIZE = 512 * 1024
|
CHUNK_SIZE = 512 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip_commit(message):
|
||||||
|
return '[skip build]' in message or '[build skip]' in message
|
||||||
|
|
||||||
|
|
||||||
class BuildArchiveException(Exception):
|
class BuildArchiveException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -36,6 +39,9 @@ class TriggerDeactivationException(Exception):
|
||||||
class ValidationRequestException(Exception):
|
class ValidationRequestException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class SkipRequestException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class EmptyRepositoryException(Exception):
|
class EmptyRepositoryException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -160,7 +166,7 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
try:
|
try:
|
||||||
hook = to_add_webhook.create_hook('web', webhook_config)
|
hook = to_add_webhook.create_hook('web', webhook_config)
|
||||||
config['hook_id'] = hook.id
|
config['hook_id'] = hook.id
|
||||||
config['master_branch'] = to_add_webhook.master_branch
|
config['master_branch'] = to_add_webhook.default_branch
|
||||||
except GithubException:
|
except GithubException:
|
||||||
msg = 'Unable to create webhook on repository: %s'
|
msg = 'Unable to create webhook on repository: %s'
|
||||||
raise TriggerActivationException(msg % new_build_source)
|
raise TriggerActivationException(msg % new_build_source)
|
||||||
|
@ -219,7 +225,7 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repo = gh_client.get_repo(source)
|
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)
|
commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)
|
||||||
|
|
||||||
return [os.path.dirname(elem.path) for elem in commit_tree.tree
|
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)
|
gh_client = self._get_client(auth_token)
|
||||||
try:
|
try:
|
||||||
repo = gh_client.get_repo(source)
|
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)
|
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
|
||||||
except GithubException as ge:
|
except GithubException as ge:
|
||||||
return None
|
return None
|
||||||
|
@ -292,7 +298,7 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
# compute the tag(s)
|
# compute the tag(s)
|
||||||
branch = ref.split('/')[-1]
|
branch = ref.split('/')[-1]
|
||||||
tags = {branch}
|
tags = {branch}
|
||||||
if branch == repo.master_branch:
|
if branch == repo.default_branch:
|
||||||
tags.add('latest')
|
tags.add('latest')
|
||||||
logger.debug('Pushing to tags: %s' % tags)
|
logger.debug('Pushing to tags: %s' % tags)
|
||||||
|
|
||||||
|
@ -309,13 +315,20 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
|
|
||||||
def handle_trigger_request(self, request, auth_token, config):
|
def handle_trigger_request(self, request, auth_token, config):
|
||||||
payload = request.get_json()
|
payload = request.get_json()
|
||||||
|
if not payload:
|
||||||
|
raise SkipRequestException()
|
||||||
|
|
||||||
if 'zen' in payload:
|
if 'zen' in payload:
|
||||||
raise ValidationRequestException()
|
raise ValidationRequestException()
|
||||||
|
|
||||||
logger.debug('Payload %s', payload)
|
logger.debug('Payload %s', payload)
|
||||||
ref = payload['ref']
|
ref = payload['ref']
|
||||||
commit_sha = payload['head_commit']['id']
|
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)
|
short_sha = GithubBuildTrigger.get_display_name(commit_sha)
|
||||||
|
|
||||||
gh_client = self._get_client(auth_token)
|
gh_client = self._get_client(auth_token)
|
||||||
|
@ -334,9 +347,9 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
|
|
||||||
gh_client = self._get_client(auth_token)
|
gh_client = self._get_client(auth_token)
|
||||||
repo = gh_client.get_repo(source)
|
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
|
master_sha = master.commit.sha
|
||||||
short_sha = GithubBuildTrigger.get_display_name(master_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)
|
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||||
Blueprint)
|
Blueprint, send_from_directory)
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.model.oauth import DatabaseAuthorizationProvider
|
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.auth import require_session_login
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
from util.seo import render_snapshot
|
from util.seo import render_snapshot
|
||||||
from util.cache import no_cache
|
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 endpoints.csrf import csrf_protect, generate_csrf_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
web = Blueprint('web', __name__)
|
web = Blueprint('web', __name__)
|
||||||
|
@ -55,6 +56,7 @@ def snapshot(path = ''):
|
||||||
|
|
||||||
@web.route('/plans/')
|
@web.route('/plans/')
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@route_show_if(features.BILLING)
|
||||||
def plans():
|
def plans():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
@ -83,6 +85,12 @@ def organizations():
|
||||||
def user():
|
def user():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
@web.route('/superuser/')
|
||||||
|
@no_cache
|
||||||
|
@route_show_if(features.SUPER_USERS)
|
||||||
|
def superuser():
|
||||||
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@web.route('/signin/')
|
@web.route('/signin/')
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@ -152,7 +160,14 @@ def privacy():
|
||||||
return render_page_template('privacy.html')
|
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'])
|
@web.route('/receipt', methods=['GET'])
|
||||||
|
@route_show_if(features.BILLING)
|
||||||
@require_session_login
|
@require_session_login
|
||||||
def receipt():
|
def receipt():
|
||||||
if not current_user.is_authenticated():
|
if not current_user.is_authenticated():
|
||||||
|
@ -298,7 +313,8 @@ def request_authorization_code():
|
||||||
if not current_app:
|
if not current_app:
|
||||||
abort(404)
|
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.
|
# Load the scope information.
|
||||||
scope_info = scopes.get_scope_information(scope)
|
scope_info = scopes.get_scope_information(scope)
|
||||||
|
@ -320,8 +336,9 @@ def request_authorization_code():
|
||||||
|
|
||||||
# Show the authorization page.
|
# Show the authorization page.
|
||||||
return render_page_template('oauthorize.html', scopes=scope_info, application=oauth_app_view,
|
return render_page_template('oauthorize.html', scopes=scope_info, application=oauth_app_view,
|
||||||
enumerate=enumerate, client_id=client_id, redirect_uri=redirect_uri,
|
enumerate=enumerate, client_id=client_id,
|
||||||
scope=scope, csrf_token_val=generate_csrf_token())
|
redirect_uri=redirect_uri, scope=scope,
|
||||||
|
csrf_token_val=generate_csrf_token())
|
||||||
|
|
||||||
if response_type == 'token':
|
if response_type == 'token':
|
||||||
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
|
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import request, make_response, Blueprint
|
from flask import request, make_response, Blueprint
|
||||||
|
|
||||||
|
from app import billing as stripe
|
||||||
from data import model
|
from data import model
|
||||||
from data.queue import dockerfile_build_queue
|
|
||||||
from auth.auth import process_auth
|
from auth.auth import process_auth
|
||||||
from auth.permissions import ModifyRepositoryPermission
|
from auth.permissions import ModifyRepositoryPermission
|
||||||
from util.invoice import renderInvoiceToHtml
|
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.names import parse_repository_name
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from endpoints.trigger import BuildTrigger, ValidationRequestException
|
from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException
|
||||||
from endpoints.common import start_build
|
from endpoints.common import start_build
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,22 +25,44 @@ def stripe_webhook():
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
logger.debug('Stripe webhook call: %s' % request_data)
|
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
|
event_type = request_data['type'] if 'type' in request_data else None
|
||||||
if event_type == 'charge.succeeded':
|
if event_type == 'charge.succeeded':
|
||||||
data = request_data['data'] if 'data' in request_data else {}
|
invoice_id = request_data['data']['object']['invoice']
|
||||||
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
|
|
||||||
|
|
||||||
if invoice_id and customer_id:
|
if user and user.invoice_email:
|
||||||
# Find the user associated with the customer ID.
|
# Lookup the invoice.
|
||||||
user = model.get_user_or_org_by_customer_id(customer_id)
|
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||||
if user and user.invoice_email:
|
if invoice:
|
||||||
# Lookup the invoice.
|
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
send_invoice_email(user.email, invoice_html)
|
||||||
if invoice:
|
|
||||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
elif event_type.startswith('customer.subscription.'):
|
||||||
send_invoice_email(user.email, invoice_html)
|
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')
|
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
|
# This was just a validation request, we don't need to build anything
|
||||||
return make_response('Okay')
|
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)
|
pull_robot_name = model.get_pull_robot_name(trigger)
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
|
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
|
||||||
|
|
31
features/__init__.py
Normal file
31
features/__init__.py
Normal file
|
@ -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__()
|
||||||
|
|
||||||
|
|
80
grunt/Gruntfile.js
Normal file
80
grunt/Gruntfile.js
Normal file
|
@ -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']);
|
||||||
|
};
|
11
grunt/package.json
Normal file
11
grunt/package.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
14
initdb.py
14
initdb.py
|
@ -10,11 +10,10 @@ from peewee import (SqliteDatabase, create_model_tables, drop_model_tables,
|
||||||
from data.database import *
|
from data.database import *
|
||||||
from data import model
|
from data import model
|
||||||
from data.model import oauth
|
from data.model import oauth
|
||||||
from app import app
|
from app import app, storage as store
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
store = app.config['STORAGE']
|
|
||||||
|
|
||||||
SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i
|
SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i
|
||||||
for i in range(1, 10)]
|
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
|
# Sanity check to make sure we're not killing our prod db
|
||||||
db = model.db
|
db = model.db
|
||||||
if (not isinstance(model.db, SqliteDatabase) or
|
if not isinstance(model.db, SqliteDatabase):
|
||||||
app.config['DB_DRIVER'] is not SqliteDatabase):
|
|
||||||
raise RuntimeError('Attempted to wipe production database!')
|
raise RuntimeError('Attempted to wipe production database!')
|
||||||
|
|
||||||
global db_initialized_for_testing
|
global db_initialized_for_testing
|
||||||
|
@ -198,6 +196,8 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='push_repo')
|
LogEntryKind.create(name='push_repo')
|
||||||
LogEntryKind.create(name='pull_repo')
|
LogEntryKind.create(name='pull_repo')
|
||||||
LogEntryKind.create(name='delete_repo')
|
LogEntryKind.create(name='delete_repo')
|
||||||
|
LogEntryKind.create(name='create_tag')
|
||||||
|
LogEntryKind.create(name='move_tag')
|
||||||
LogEntryKind.create(name='delete_tag')
|
LogEntryKind.create(name='delete_tag')
|
||||||
LogEntryKind.create(name='add_repo_permission')
|
LogEntryKind.create(name='add_repo_permission')
|
||||||
LogEntryKind.create(name='change_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
|
# Sanity check to make sure we're not killing our prod db
|
||||||
db = model.db
|
db = model.db
|
||||||
if (not isinstance(model.db, SqliteDatabase) or
|
if not isinstance(model.db, SqliteDatabase):
|
||||||
app.config['DB_DRIVER'] is not SqliteDatabase):
|
|
||||||
raise RuntimeError('Attempted to wipe production database!')
|
raise RuntimeError('Attempted to wipe production database!')
|
||||||
|
|
||||||
drop_model_tables(all_models, fail_silently=True)
|
drop_model_tables(all_models, fail_silently=True)
|
||||||
|
@ -490,7 +489,8 @@ def populate_database():
|
||||||
'service': trigger.service.name})
|
'service': trigger.service.name})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.config['LOGGING_CONFIG']()
|
log_level = getattr(logging, app.config['LOGGING_LEVEL'])
|
||||||
|
logging.basicConfig(level=log_level)
|
||||||
initialize_database()
|
initialize_database()
|
||||||
|
|
||||||
if app.config.get('POPULATE_DB_TEST_DATA', False):
|
if app.config.get('POPULATE_DB_TEST_DATA', False):
|
||||||
|
|
|
@ -18,7 +18,6 @@ python-daemon
|
||||||
paramiko
|
paramiko
|
||||||
python-digitalocean
|
python-digitalocean
|
||||||
xhtml2pdf
|
xhtml2pdf
|
||||||
logstash_formatter
|
|
||||||
redis
|
redis
|
||||||
hiredis
|
hiredis
|
||||||
git+https://github.com/DevTable/docker-py.git
|
git+https://github.com/DevTable/docker-py.git
|
||||||
|
@ -26,4 +25,10 @@ loremipsum
|
||||||
pygithub
|
pygithub
|
||||||
flask-restful
|
flask-restful
|
||||||
jsonschema
|
jsonschema
|
||||||
git+https://github.com/NateFerrero/oauth2lib.git
|
git+https://github.com/NateFerrero/oauth2lib.git
|
||||||
|
alembic
|
||||||
|
sqlalchemy
|
||||||
|
python-magic
|
||||||
|
reportlab==2.7
|
||||||
|
blinker
|
||||||
|
raven
|
||||||
|
|
|
@ -5,47 +5,49 @@ Flask-Mail==0.9.0
|
||||||
Flask-Principal==0.4.0
|
Flask-Principal==0.4.0
|
||||||
Flask-RESTful==0.2.12
|
Flask-RESTful==0.2.12
|
||||||
Jinja2==2.7.2
|
Jinja2==2.7.2
|
||||||
MarkupSafe==0.19
|
Mako==0.9.1
|
||||||
Pillow==2.3.1
|
MarkupSafe==0.21
|
||||||
|
Pillow==2.4.0
|
||||||
PyGithub==1.24.1
|
PyGithub==1.24.1
|
||||||
PyMySQL==0.6.1
|
PyMySQL==0.6.2
|
||||||
|
PyPDF2==1.21
|
||||||
|
SQLAlchemy==0.9.4
|
||||||
Werkzeug==0.9.4
|
Werkzeug==0.9.4
|
||||||
|
alembic==0.6.4
|
||||||
aniso8601==0.82
|
aniso8601==0.82
|
||||||
argparse==1.2.1
|
argparse==1.2.1
|
||||||
beautifulsoup4==4.3.2
|
beautifulsoup4==4.3.2
|
||||||
blinker==1.3
|
blinker==1.3
|
||||||
boto==2.27.0
|
boto==2.27.0
|
||||||
distribute==0.6.34
|
|
||||||
git+https://github.com/DevTable/docker-py.git
|
git+https://github.com/DevTable/docker-py.git
|
||||||
ecdsa==0.11
|
ecdsa==0.11
|
||||||
gevent==1.0
|
gevent==1.0.1
|
||||||
greenlet==0.4.2
|
greenlet==0.4.2
|
||||||
gunicorn==18.0
|
gunicorn==18.0
|
||||||
hiredis==0.1.2
|
hiredis==0.1.3
|
||||||
html5lib==1.0b3
|
html5lib==0.999
|
||||||
itsdangerous==0.23
|
itsdangerous==0.24
|
||||||
jsonschema==2.3.0
|
jsonschema==2.3.0
|
||||||
lockfile==0.9.1
|
lockfile==0.9.1
|
||||||
logstash-formatter==0.5.8
|
|
||||||
loremipsum==1.0.2
|
loremipsum==1.0.2
|
||||||
marisa-trie==0.6
|
marisa-trie==0.6
|
||||||
mixpanel-py==3.1.2
|
mixpanel-py==3.1.2
|
||||||
mock==1.0.1
|
|
||||||
git+https://github.com/NateFerrero/oauth2lib.git
|
git+https://github.com/NateFerrero/oauth2lib.git
|
||||||
paramiko==1.13.0
|
paramiko==1.13.0
|
||||||
peewee==2.2.2
|
peewee==2.2.3
|
||||||
py-bcrypt==0.4
|
py-bcrypt==0.4
|
||||||
pyPdf==1.13
|
|
||||||
pycrypto==2.6.1
|
pycrypto==2.6.1
|
||||||
python-daemon==1.6
|
python-daemon==1.6
|
||||||
python-dateutil==2.2
|
python-dateutil==2.2
|
||||||
python-digitalocean==0.7
|
python-digitalocean==0.7
|
||||||
|
python-magic==0.4.6
|
||||||
pytz==2014.2
|
pytz==2014.2
|
||||||
|
raven==4.2.1
|
||||||
redis==2.9.1
|
redis==2.9.1
|
||||||
reportlab==2.7
|
reportlab==2.7
|
||||||
requests==2.2.1
|
requests==2.2.1
|
||||||
six==1.6.1
|
six==1.6.1
|
||||||
stripe==1.12.2
|
stripe==1.14.0
|
||||||
websocket-client==0.11.0
|
websocket-client==0.11.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
xhtml2pdf==0.0.5
|
xhtml2pdf==0.0.6
|
||||||
|
|
|
@ -676,6 +676,10 @@ i.toggle-icon:hover {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phase-icon.pulling {
|
||||||
|
background-color: #cab442;
|
||||||
|
}
|
||||||
|
|
||||||
.phase-icon.building {
|
.phase-icon.building {
|
||||||
background-color: #f0ad4e;
|
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 {
|
.plans-list .plan-box .description {
|
||||||
color: white;
|
color: white;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
@ -1528,22 +1550,22 @@ p.editable:hover i {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings {
|
.tag-specific-images-view .image-listings {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing {
|
.tag-specific-images-view .image-listings .image-listing {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-id {
|
.tag-specific-images-view .image-listings .image-listing .image-listing-id {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 20px;
|
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;
|
border-left: 2px solid steelblue;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -1554,15 +1576,15 @@ p.editable:hover i {
|
||||||
z-index: 1;
|
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;
|
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;
|
bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-circle {
|
.tag-specific-images-view .image-listings .image-listing .image-listing-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|
||||||
|
@ -1575,14 +1597,55 @@ p.editable:hover i {
|
||||||
z-index: 2;
|
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;
|
background: steelblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmdeleteTagModal .more-changes {
|
.tag-specific-images-view .more-changes {
|
||||||
margin-left: 16px;
|
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 {
|
.repo .header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
|
@ -1644,6 +1707,10 @@ p.editable:hover i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo .repo-controls .dropdown {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.repo .repo-controls .count {
|
.repo .repo-controls .count {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
|
@ -1798,6 +1865,77 @@ p.editable:hover i {
|
||||||
text-decoration: none !important;
|
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 {
|
.repo-list {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
@ -2106,19 +2244,11 @@ p.editable:hover i {
|
||||||
margin: 0px;
|
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 {
|
.repo .formatted-command {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .formatted-command.trimmed {
|
.repo .formatted-command.trimmed {
|
||||||
|
@ -2127,16 +2257,22 @@ p.editable:hover i {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .changes-count-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo .change-count {
|
.repo .change-count {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 10px;
|
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 {
|
.repo .changes-container i.fa-plus-square {
|
||||||
color: rgb(73, 209, 73);
|
color: rgb(73, 209, 73);
|
||||||
}
|
}
|
||||||
|
@ -2154,7 +2290,7 @@ p.editable:hover i {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .change-count i {
|
.repo .change-count i {
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2166,6 +2302,7 @@ p.editable:hover i {
|
||||||
|
|
||||||
.repo .more-changes {
|
.repo .more-changes {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo #collapseChanges .well {
|
.repo #collapseChanges .well {
|
||||||
|
@ -2316,11 +2453,6 @@ p.editable:hover i {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-admin .form-change input {
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-admin .convert-form h3 {
|
.user-admin .convert-form h3 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
@ -2411,10 +2543,13 @@ p.editable:hover i {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags .tag, #confirmdeleteTagModal .tag {
|
.tags .tag, .tag-specific-images-view .tag {
|
||||||
|
display: inline-block;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-tags {
|
.tooltip-tags {
|
||||||
|
@ -2464,42 +2599,42 @@ p.editable:hover i {
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart {
|
.usage-chart {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart .count-text {
|
.usage-chart .count-text {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-at path.arc-0 {
|
.usage-chart.limit-at path.arc-0 {
|
||||||
fill: #c09853;
|
fill: #c09853;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-over path.arc-0 {
|
.usage-chart.limit-over path.arc-0 {
|
||||||
fill: #b94a48;
|
fill: #b94a48;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-near path.arc-0 {
|
.usage-chart.limit-near path.arc-0 {
|
||||||
fill: #468847;
|
fill: #468847;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-over path.arc-1 {
|
.usage-chart.limit-over path.arc-1 {
|
||||||
fill: #fcf8e3;
|
fill: #fcf8e3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-at path.arc-1 {
|
.usage-chart.limit-at path.arc-1 {
|
||||||
fill: #f2dede;
|
fill: #f2dede;
|
||||||
}
|
}
|
||||||
|
|
||||||
#repository-usage-chart.limit-near path.arc-1 {
|
.usage-chart.limit-near path.arc-1 {
|
||||||
fill: #dff0d8;
|
fill: #dff0d8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-manager-element .usage-caption {
|
.usage-caption {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
|
@ -3630,4 +3765,17 @@ pre.command:before {
|
||||||
|
|
||||||
.trigger-option-section table td {
|
.trigger-option-section table td {
|
||||||
padding: 6px;
|
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;
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
|
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
|
||||||
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
||||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
|
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
|
||||||
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
|
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-require="['BILLING']">Pricing</a></li>
|
||||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||||
|
<li ng-if="user.super_user"><a href="/superuser/"><strong>Super User Admin Panel</strong></a></li>
|
||||||
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
|
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -17,10 +17,15 @@
|
||||||
You are nearing the number of allowed private repositories. It might be time to think about
|
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 <span ng-show="organization">organization's</span> service.
|
upgrading your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Trial info -->
|
||||||
|
<div class="alert alert-success" ng-show="subscription.trialEnd != null" style="font-size: 125%">
|
||||||
|
Free trial until <strong>{{ parseDate(subscription.trialEnd) | date }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div>
|
<div>
|
||||||
<div id="repository-usage-chart" class="limit-{{limit}}"></div>
|
<div id="repository-usage-chart" class="usage-chart limit-{{limit}}"></div>
|
||||||
<span class="usage-caption" ng-show="chart">Repository Usage</span>
|
<span class="usage-caption" ng-show="chart">Repository Usage</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -57,7 +62,8 @@
|
||||||
ng-click="changeSubscription(plan.stripeId)">
|
ng-click="changeSubscription(plan.stripeId)">
|
||||||
<span class="quay-spinner" ng-show="planChanging"></span>
|
<span class="quay-spinner" ng-show="planChanging"></span>
|
||||||
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
|
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
|
||||||
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
|
<span ng-show="!planChanging && subscribedPlan.price == 0 && !isExistingCustomer">Start Free Trial</span>
|
||||||
|
<span ng-show="!planChanging && subscribedPlan.price == 0 && isExistingCustomer">Subscribe</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
||||||
ng-click="cancelSubscription()">
|
ng-click="cancelSubscription()">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<button class="btn btn-success" data-trigger="click"
|
<button class="btn btn-success" data-trigger="click"
|
||||||
data-content-template="static/directives/popup-input-dialog.html"
|
data-content-template="/static/directives/popup-input-dialog.html"
|
||||||
data-placement="bottom" ng-click="popupShown()" bs-popover>
|
data-placement="bottom" ng-click="popupShown()" bs-popover>
|
||||||
<span ng-transclude></span>
|
<span ng-transclude></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -8,7 +8,10 @@
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
<h4 class="modal-title">Setup new build trigger</h4>
|
<h4 class="modal-title">Setup new build trigger</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" ng-show="activating">
|
||||||
|
<span class="quay-spinner"></span> Setting up trigger...
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" ng-show="!activating">
|
||||||
<!-- Trigger-specific setup -->
|
<!-- Trigger-specific setup -->
|
||||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||||
<div ng-switch-when="github">
|
<div ng-switch-when="github">
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
The
|
The
|
||||||
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
|
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
|
||||||
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
|
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
|
||||||
depends on repository
|
depends on Quay.io repository
|
||||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
|
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
|
||||||
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
||||||
</a> which requires
|
</a> which requires
|
||||||
|
@ -45,7 +48,7 @@
|
||||||
<table style="width: 100%;" ng-show="pullRequirements">
|
<table style="width: 100%;" ng-show="pullRequirements">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 114px">
|
<td style="width: 114px">
|
||||||
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images from Quay.io" bs-tooltip>
|
||||||
Pull Credentials:
|
Pull Credentials:
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -76,10 +79,10 @@
|
||||||
filter="['robot']"></div>
|
filter="['robot']"></div>
|
||||||
|
|
||||||
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||||
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the repository.
|
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the Quay.io repository.
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-warning" ng-if="!pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
<div class="alert alert-warning" ng-if="!pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||||
Note: No robot account currently has access to the repository. Please create one and/or assign access in the
|
Note: No robot account currently has access to the Quay.io repository. Please create one and/or assign access in the
|
||||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
|
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -90,8 +93,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary"
|
<button type="button" class="btn btn-primary"
|
||||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
|
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements || activating"
|
||||||
ng-click="activate">Finished</button>
|
ng-click="activate()">Finished</button>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
placeholder="Password" ng-model="user.password">
|
placeholder="Password" ng-model="user.password">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||||
|
|
||||||
<span class="social-alternate">
|
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
|
||||||
<i class="fa fa-circle"></i>
|
<i class="fa fa-circle"></i>
|
||||||
<span class="inner-text">OR</span>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()">
|
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()"
|
||||||
|
quay-require="['GITHUB_LOGIN']">
|
||||||
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
|
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -10,14 +10,19 @@
|
||||||
<div class="form-group signin-buttons">
|
<div class="form-group signin-buttons">
|
||||||
<button id="signupButton"
|
<button id="signupButton"
|
||||||
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||||
analytics-on analytics-event="register">Sign Up for Free!</button>
|
analytics-on analytics-event="register">
|
||||||
<span class="social-alternate">
|
<span quay-show="Features.BILLING">Sign Up for Free!</span>
|
||||||
|
<span quay-show="!Features.BILLING">Sign Up</span>
|
||||||
|
</button>
|
||||||
|
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
|
||||||
<i class="fa fa-circle"></i>
|
<i class="fa fa-circle"></i>
|
||||||
<span class="inner-text">OR</span>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}"
|
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}"
|
||||||
class="btn btn-primary btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
|
class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']">
|
||||||
<p class="help-block">No credit card required.</p>
|
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
|
||||||
|
</a>
|
||||||
|
<p class="help-block" quay-require="['BILLING']">No credit card required.</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div ng-show="registering" style="text-align: center">
|
<div ng-show="registering" style="text-align: center">
|
||||||
|
|
17
static/directives/tag-specific-images-view.html
Normal file
17
static/directives/tag-specific-images-view.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="tag-specific-images-view-element" ng-show="tagSpecificImages.length">
|
||||||
|
<div ng-transclude></div>
|
||||||
|
<div class="image-listings">
|
||||||
|
<div class="image-listing" ng-repeat="image in tagSpecificImages | limitTo:5"
|
||||||
|
ng-class="getImageListingClasses(image)">
|
||||||
|
<span class="image-listing-circle"></span>
|
||||||
|
<span class="image-listing-line"></span>
|
||||||
|
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="getFirstTextLine(image.comment)"
|
||||||
|
data-html="true">
|
||||||
|
{{ image.id.substr(0, 12) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="more-changes" ng-show="tagSpecificImages.length > 5">
|
||||||
|
And {{ tagSpecificImages.length - 5 }} more...
|
||||||
|
</div>
|
||||||
|
</div>
|
642
static/js/app.js
642
static/js/app.js
File diff suppressed because it is too large
Load diff
13
static/js/bootstrap.js
vendored
13
static/js/bootstrap.js
vendored
|
@ -1,13 +0,0 @@
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
async: false,
|
|
||||||
url: '/api/discovery',
|
|
||||||
success: function(data) {
|
|
||||||
window.__endpoints = data.endpoints;
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
$('#couldnotloadModal').modal({});
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -48,14 +48,15 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
|
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Config) {
|
||||||
// Default to showing sudo on all commands if on linux.
|
// Default to showing sudo on all commands if on linux.
|
||||||
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
|
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
|
||||||
|
|
||||||
$scope.tour = {
|
$scope.tour = {
|
||||||
'title': 'Quay.io Tutorial',
|
'title': 'Quay.io Tutorial',
|
||||||
'initialScope': {
|
'initialScope': {
|
||||||
'showSudo': showSudo
|
'showSudo': showSudo,
|
||||||
|
'domainName': Config.getDomain()
|
||||||
},
|
},
|
||||||
'steps': [
|
'steps': [
|
||||||
{
|
{
|
||||||
|
@ -262,7 +263,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
|
||||||
loadPublicRepos();
|
loadPublicRepos();
|
||||||
}
|
}
|
||||||
|
|
||||||
function LandingCtrl($scope, UserService, ApiService) {
|
function LandingCtrl($scope, UserService, ApiService, Features, Config) {
|
||||||
$scope.namespace = null;
|
$scope.namespace = null;
|
||||||
|
|
||||||
$scope.$watch('namespace', function(namespace) {
|
$scope.$watch('namespace', function(namespace) {
|
||||||
|
@ -303,10 +304,22 @@ function LandingCtrl($scope, UserService, ApiService) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
browserchrome.update();
|
$scope.chromify = function() {
|
||||||
|
browserchrome.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getEnterpriseLogo = function() {
|
||||||
|
if (!Config.ENTERPRISE_LOGO_URL) {
|
||||||
|
return '/static/img/quay-logo.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Config.ENTERPRISE_LOGO_URL;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout) {
|
function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config) {
|
||||||
|
$scope.Config = Config;
|
||||||
|
|
||||||
var namespace = $routeParams.namespace;
|
var namespace = $routeParams.namespace;
|
||||||
var name = $routeParams.name;
|
var name = $routeParams.name;
|
||||||
|
|
||||||
|
@ -349,6 +362,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.handleBuildStarted = function(build) {
|
$scope.handleBuildStarted = function(build) {
|
||||||
|
getBuildInfo($scope.repo);
|
||||||
startBuildInfoTimer($scope.repo);
|
startBuildInfoTimer($scope.repo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -384,9 +398,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
$scope.getMoreCount = function(changes) {
|
$scope.getMoreCount = function(changes) {
|
||||||
if (!changes) { return 0; }
|
if (!changes) { return 0; }
|
||||||
var addedDisplayed = Math.min(5, changes.added.length);
|
var addedDisplayed = Math.min(2, changes.added.length);
|
||||||
var removedDisplayed = Math.min(5, changes.removed.length);
|
var removedDisplayed = Math.min(2, changes.removed.length);
|
||||||
var changedDisplayed = Math.min(5, changes.changed.length);
|
var changedDisplayed = Math.min(2, changes.changed.length);
|
||||||
|
|
||||||
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
||||||
addedDisplayed - removedDisplayed - changedDisplayed;
|
addedDisplayed - removedDisplayed - changedDisplayed;
|
||||||
|
@ -415,57 +429,21 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
$location.search('tag', null);
|
$location.search('tag', null);
|
||||||
$location.search('image', imageId.substr(0, 12));
|
$location.search('image', imageId.substr(0, 12));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showAddTag = function(image) {
|
||||||
|
$scope.toTagImage = image;
|
||||||
|
$('#addTagModal').modal('show');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.tagSpecificImages = function(tagName) {
|
$scope.isOwnedTag = function(image, tagName) {
|
||||||
if (!tagName) { return []; }
|
if (!image || !tagName) { return false; }
|
||||||
|
return image.tags.indexOf(tagName) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
var tag = $scope.repo.tags[tagName];
|
$scope.isAnotherImageTag = function(image, tagName) {
|
||||||
if (!tag) { return []; }
|
if (!image || !tagName) { return false; }
|
||||||
|
return image.tags.indexOf(tagName) < 0 && $scope.repo.tags[tagName];
|
||||||
if ($scope.specificImages && $scope.specificImages[tagName]) {
|
|
||||||
return $scope.specificImages[tagName];
|
|
||||||
}
|
|
||||||
|
|
||||||
var getIdsForTag = function(currentTag) {
|
|
||||||
var ids = {};
|
|
||||||
forAllTagImages(currentTag, function(image) {
|
|
||||||
ids[image.dbid] = true;
|
|
||||||
});
|
|
||||||
return ids;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove any IDs that match other tags.
|
|
||||||
var toDelete = getIdsForTag(tag);
|
|
||||||
for (var currentTagName in $scope.repo.tags) {
|
|
||||||
var currentTag = $scope.repo.tags[currentTagName];
|
|
||||||
if (currentTag != tag) {
|
|
||||||
for (var dbid in getIdsForTag(currentTag)) {
|
|
||||||
delete toDelete[dbid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matching list of images.
|
|
||||||
var images = [];
|
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
|
||||||
var image = $scope.images[i];
|
|
||||||
if (toDelete[image.dbid]) {
|
|
||||||
images.push(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
images.sort(function(a, b) {
|
|
||||||
var result = new Date(b.created) - new Date(a.created);
|
|
||||||
if (result != 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.dbid - a.dbid;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.specificImages[tagName] = images;
|
|
||||||
return images;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.askDeleteTag = function(tagName) {
|
$scope.askDeleteTag = function(tagName) {
|
||||||
|
@ -475,6 +453,39 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
$('#confirmdeleteTagModal').modal('show');
|
$('#confirmdeleteTagModal').modal('show');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.createOrMoveTag = function(image, tagName, opt_invalid) {
|
||||||
|
if (opt_invalid) { return; }
|
||||||
|
|
||||||
|
$scope.creatingTag = true;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repo.namespace + '/' + $scope.repo.name,
|
||||||
|
'tag': tagName
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'image': image.id
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.changeTagImage(data, params).then(function(resp) {
|
||||||
|
$scope.creatingTag = false;
|
||||||
|
loadViewInfo();
|
||||||
|
$('#addTagModal').modal('hide');
|
||||||
|
}, function(resp) {
|
||||||
|
$('#addTagModal').modal('hide');
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": resp.data ? resp.data : 'Could not create or move tag',
|
||||||
|
"title": "Cannot create or move tag",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.deleteTag = function(tagName) {
|
$scope.deleteTag = function(tagName) {
|
||||||
if (!$scope.repo.can_admin) { return; }
|
if (!$scope.repo.can_admin) { return; }
|
||||||
$('#confirmdeleteTagModal').modal('hide');
|
$('#confirmdeleteTagModal').modal('hide');
|
||||||
|
@ -555,20 +566,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
$scope.getFirstTextLine = getFirstTextLine;
|
$scope.getFirstTextLine = getFirstTextLine;
|
||||||
|
|
||||||
$scope.getImageListingClasses = function(image, tagName) {
|
|
||||||
var classes = '';
|
|
||||||
if (image.ancestors.length > 1) {
|
|
||||||
classes += 'child ';
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentTag = $scope.repo.tags[tagName];
|
|
||||||
if (image.dbid == currentTag.image.dbid) {
|
|
||||||
classes += 'tag-image ';
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getTagCount = function(repo) {
|
$scope.getTagCount = function(repo) {
|
||||||
if (!repo) { return 0; }
|
if (!repo) { return 0; }
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
@ -734,33 +731,34 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the new tree.
|
// Create the new tree.
|
||||||
$scope.tree = new ImageHistoryTree(namespace, name, resp.images,
|
var tree = new ImageHistoryTree(namespace, name, resp.images,
|
||||||
getFirstTextLine, $scope.getTimeSince, ImageMetadataService.getEscapedFormattedCommand);
|
getFirstTextLine, $scope.getTimeSince, ImageMetadataService.getEscapedFormattedCommand);
|
||||||
|
|
||||||
$scope.tree.draw('image-history-container');
|
$scope.tree = tree.draw('image-history-container');
|
||||||
|
if ($scope.tree) {
|
||||||
|
// If we already have a tag, use it
|
||||||
|
if ($scope.currentTag) {
|
||||||
|
$scope.tree.setTag($scope.currentTag.name);
|
||||||
|
}
|
||||||
|
|
||||||
// If we already have a tag, use it
|
// Listen for changes to the selected tag and image in the tree.
|
||||||
if ($scope.currentTag) {
|
$($scope.tree).bind('tagChanged', function(e) {
|
||||||
$scope.tree.setTag($scope.currentTag.name);
|
$scope.$apply(function() { $scope.setTag(e.tag, true); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$($scope.tree).bind('imageChanged', function(e) {
|
||||||
|
$scope.$apply(function() { $scope.setImage(e.image.id, true); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$($scope.tree).bind('showTagMenu', function(e) {
|
||||||
|
$scope.$apply(function() { $scope.showTagMenu(e.tag, e.clientX, e.clientY); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$($scope.tree).bind('hideTagMenu', function(e) {
|
||||||
|
$scope.$apply(function() { $scope.hideTagMenu(); });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes to the selected tag and image in the tree.
|
|
||||||
$($scope.tree).bind('tagChanged', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.setTag(e.tag, true); });
|
|
||||||
});
|
|
||||||
|
|
||||||
$($scope.tree).bind('imageChanged', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.setImage(e.image.id, true); });
|
|
||||||
});
|
|
||||||
|
|
||||||
$($scope.tree).bind('showTagMenu', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.showTagMenu(e.tag, e.clientX, e.clientY); });
|
|
||||||
});
|
|
||||||
|
|
||||||
$($scope.tree).bind('hideTagMenu', function(e) {
|
|
||||||
$scope.$apply(function() { $scope.hideTagMenu(); });
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($routeParams.image) {
|
if ($routeParams.image) {
|
||||||
$scope.setImage($routeParams.image);
|
$scope.setImage($routeParams.image);
|
||||||
}
|
}
|
||||||
|
@ -844,7 +842,7 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
|
||||||
if (dockerfile && dockerfile.canRead) {
|
if (dockerfile && dockerfile.canRead) {
|
||||||
DataFileService.blobToString(dockerfile.toBlob(), function(result) {
|
DataFileService.blobToString(dockerfile.toBlob(), function(result) {
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
$scope.dockerFilePath = dockerfilePath;
|
$scope.dockerFilePath = dockerfilePath || 'Dockerfile';
|
||||||
$scope.dockerFileContents = result;
|
$scope.dockerFileContents = result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -854,8 +852,11 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
|
||||||
};
|
};
|
||||||
|
|
||||||
var notarchive = function() {
|
var notarchive = function() {
|
||||||
$scope.dockerFileContents = DataFileService.arrayToString(uint8array);
|
DataFileService.arrayToString(uint8array, function(r) {
|
||||||
$scope.loaded = true;
|
$scope.dockerFilePath = 'Dockerfile';
|
||||||
|
$scope.dockerFileContents = r;
|
||||||
|
$scope.loaded = true;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
DataFileService.readDataArrayAsPossibleArchive(uint8array, archiveread, notarchive);
|
DataFileService.readDataArrayAsPossibleArchive(uint8array, archiveread, notarchive);
|
||||||
|
@ -1094,6 +1095,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||||
// root build object to remain the same object.
|
// root build object to remain the same object.
|
||||||
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
|
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
|
||||||
|
var currentBuild = $scope.builds[$scope.currentBuildIndex];
|
||||||
checkPollTimer();
|
checkPollTimer();
|
||||||
|
|
||||||
// Load the updated logs for the build.
|
// Load the updated logs for the build.
|
||||||
|
@ -1110,6 +1112,18 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
processLogs(resp['logs'], resp['start']);
|
processLogs(resp['logs'], resp['start']);
|
||||||
$scope.logStartIndex = resp['total'];
|
$scope.logStartIndex = resp['total'];
|
||||||
$scope.polling = false;
|
$scope.polling = false;
|
||||||
|
|
||||||
|
// If the build status is an error, open the last two log entries.
|
||||||
|
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
|
||||||
|
var openLogEntries = function(entry) {
|
||||||
|
if (entry.logs) {
|
||||||
|
entry.logs.setVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
|
||||||
|
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
|
||||||
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.polling = false;
|
$scope.polling = false;
|
||||||
});
|
});
|
||||||
|
@ -1152,7 +1166,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
fetchRepository();
|
fetchRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService) {
|
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config) {
|
||||||
var namespace = $routeParams.namespace;
|
var namespace = $routeParams.namespace;
|
||||||
var name = $routeParams.name;
|
var name = $routeParams.name;
|
||||||
|
|
||||||
|
@ -1170,12 +1184,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
$scope.getBadgeFormat = function(format, repo) {
|
$scope.getBadgeFormat = function(format, repo) {
|
||||||
if (!repo) { return; }
|
if (!repo) { return; }
|
||||||
|
|
||||||
var imageUrl = 'https://quay.io/repository/' + namespace + '/' + name + '/status';
|
var imageUrl = Config.getUrl('/' + namespace + '/' + name + '/status');
|
||||||
if (!$scope.repo.is_public) {
|
if (!$scope.repo.is_public) {
|
||||||
imageUrl += '?token=' + $scope.repo.status_token;
|
imageUrl += '?token=' + $scope.repo.status_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
var linkUrl = 'https://quay.io/repository/' + namespace + '/' + name;
|
var linkUrl = Config.getUrl('/' + namespace + '/' + name);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'svg':
|
case 'svg':
|
||||||
|
@ -1492,6 +1506,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteTrigger = function(trigger) {
|
$scope.deleteTrigger = function(trigger) {
|
||||||
|
if (!trigger) { return; }
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name,
|
'repository': namespace + '/' + name,
|
||||||
'trigger_uuid': trigger.id
|
'trigger_uuid': trigger.id
|
||||||
|
@ -1559,12 +1575,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
|
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
|
||||||
$routeParams, $http, UIService) {
|
$routeParams, $http, UIService, Features) {
|
||||||
|
$scope.Features = Features;
|
||||||
|
|
||||||
if ($routeParams['migrate']) {
|
if ($routeParams['migrate']) {
|
||||||
$('#migrateTab').tab('show')
|
$('#migrateTab').tab('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
UserService.updateUserIn($scope, function(user) {
|
UserService.updateUserIn($scope, function(user) {
|
||||||
|
if (!Features.GITHUB_LOGIN) { return; }
|
||||||
|
|
||||||
$scope.cuser = jQuery.extend({}, user);
|
$scope.cuser = jQuery.extend({}, user);
|
||||||
|
|
||||||
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
||||||
|
@ -1589,7 +1609,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
$scope.convertStep = 0;
|
$scope.convertStep = 0;
|
||||||
$scope.org = {};
|
$scope.org = {};
|
||||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||||
$scope.githubClientId = KeyService.githubClientId;
|
$scope.githubClientId = KeyService.githubLoginClientId;
|
||||||
$scope.authorizedApps = null;
|
$scope.authorizedApps = null;
|
||||||
|
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
|
@ -1640,13 +1660,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.showConvertForm = function() {
|
$scope.showConvertForm = function() {
|
||||||
PlanService.getMatchingBusinessPlan(function(plan) {
|
if (Features.BILLING) {
|
||||||
$scope.org.plan = plan;
|
PlanService.getMatchingBusinessPlan(function(plan) {
|
||||||
});
|
$scope.org.plan = plan;
|
||||||
|
});
|
||||||
|
|
||||||
PlanService.getPlans(function(plans) {
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.orgPlans = plans;
|
$scope.orgPlans = plans;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.convertStep = 1;
|
$scope.convertStep = 1;
|
||||||
};
|
};
|
||||||
|
@ -1661,7 +1683,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
var data = {
|
var data = {
|
||||||
'adminUser': $scope.org.adminUser,
|
'adminUser': $scope.org.adminUser,
|
||||||
'adminPassword': $scope.org.adminPassword,
|
'adminPassword': $scope.org.adminPassword,
|
||||||
'plan': $scope.org.plan.stripeId
|
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.convertUserToOrganization(data).then(function(resp) {
|
ApiService.convertUserToOrganization(data).then(function(resp) {
|
||||||
|
@ -1856,7 +1878,7 @@ function V1Ctrl($scope, $location, UserService) {
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) {
|
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) {
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||||
|
@ -1978,13 +2000,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
||||||
var checkPrivateAllowed = function() {
|
var checkPrivateAllowed = function() {
|
||||||
if (!$scope.repo || !$scope.repo.namespace) { return; }
|
if (!$scope.repo || !$scope.repo.namespace) { return; }
|
||||||
|
|
||||||
|
if (!Features.BILLING) {
|
||||||
|
$scope.checkingPlan = false;
|
||||||
|
$scope.planRequired = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.checkingPlan = true;
|
$scope.checkingPlan = true;
|
||||||
|
|
||||||
var isUserNamespace = $scope.isUserNamespace;
|
var isUserNamespace = $scope.isUserNamespace;
|
||||||
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
|
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
|
||||||
$scope.checkingPlan = false;
|
$scope.checkingPlan = false;
|
||||||
|
|
||||||
if (resp['privateAllowed']) {
|
if (resp['privateAllowed']) {
|
||||||
$scope.planRequired = null;
|
$scope.planRequired = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2104,18 +2132,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
||||||
loadOrganization();
|
loadOrganization();
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, UIService) {
|
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features, UIService) {
|
||||||
var orgname = $routeParams.orgname;
|
var orgname = $routeParams.orgname;
|
||||||
|
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
PlanService.getPlans(function(plans) {
|
if (Features.BILLING) {
|
||||||
$scope.plans = plans;
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.plan_map = {};
|
$scope.plans = plans;
|
||||||
|
$scope.plan_map = {};
|
||||||
for (var i = 0; i < plans.length; ++i) {
|
|
||||||
$scope.plan_map[plans[i].stripeId] = plans[i];
|
for (var i = 0; i < plans.length; ++i) {
|
||||||
}
|
$scope.plan_map[plans[i].stripeId] = plans[i];
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.orgname = orgname;
|
$scope.orgname = orgname;
|
||||||
$scope.membersLoading = true;
|
$scope.membersLoading = true;
|
||||||
|
@ -2297,34 +2327,43 @@ function OrgsCtrl($scope, UserService) {
|
||||||
browserchrome.update();
|
browserchrome.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) {
|
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) {
|
||||||
|
$scope.Features = Features;
|
||||||
|
$scope.holder = {};
|
||||||
|
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
var requested = $routeParams['plan'];
|
var requested = $routeParams['plan'];
|
||||||
|
|
||||||
// Load the list of plans.
|
if (Features.BILLING) {
|
||||||
PlanService.getPlans(function(plans) {
|
// Load the list of plans.
|
||||||
$scope.plans = plans;
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.currentPlan = null;
|
$scope.plans = plans;
|
||||||
if (requested) {
|
$scope.holder.currentPlan = null;
|
||||||
PlanService.getPlan(requested, function(plan) {
|
if (requested) {
|
||||||
$scope.currentPlan = plan;
|
PlanService.getPlan(requested, function(plan) {
|
||||||
});
|
$scope.holder.currentPlan = plan;
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.signedIn = function() {
|
$scope.signedIn = function() {
|
||||||
PlanService.handleNotedPlan();
|
if (Features.BILLING) {
|
||||||
|
PlanService.handleNotedPlan();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.signinStarted = function() {
|
$scope.signinStarted = function() {
|
||||||
PlanService.getMinimumPlan(1, true, function(plan) {
|
if (Features.BILLING) {
|
||||||
PlanService.notePlan(plan.stripeId);
|
PlanService.getMinimumPlan(1, true, function(plan) {
|
||||||
});
|
PlanService.notePlan(plan.stripeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setPlan = function(plan) {
|
$scope.setPlan = function(plan) {
|
||||||
$scope.currentPlan = plan;
|
$scope.holder.currentPlan = plan;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createNewOrg = function() {
|
$scope.createNewOrg = function() {
|
||||||
|
@ -2352,7 +2391,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the selected plan is free, simply move to the org page.
|
// If the selected plan is free, simply move to the org page.
|
||||||
if ($scope.currentPlan.price == 0) {
|
if (!Features.BILLING || $scope.holder.currentPlan.price == 0) {
|
||||||
showOrg();
|
showOrg();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2366,7 +2405,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
||||||
'failure': showOrg
|
'failure': showOrg
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, callbacks);
|
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
$scope.creating = false;
|
$scope.creating = false;
|
||||||
$scope.createError = result.data.message || result.data;
|
$scope.createError = result.data.message || result.data;
|
||||||
|
@ -2545,4 +2584,135 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
||||||
// Load the organization and application info.
|
// Load the organization and application info.
|
||||||
loadOrganization();
|
loadOrganization();
|
||||||
loadApplicationInfo();
|
loadApplicationInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
|
if (!Features.SUPER_USERS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor any user changes and place the current user into the scope.
|
||||||
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
|
$scope.loadUsers = function() {
|
||||||
|
if ($scope.users) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.loadUsersInternal();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.loadUsersInternal = function() {
|
||||||
|
ApiService.listAllUsers().then(function(resp) {
|
||||||
|
$scope.users = resp['users'];
|
||||||
|
}, function(resp) {
|
||||||
|
$scope.users = [];
|
||||||
|
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showChangePassword = function(user) {
|
||||||
|
$scope.userToChange = user;
|
||||||
|
$('#changePasswordModal').modal({});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showDeleteUser = function(user) {
|
||||||
|
if (user.username == UserService.currentUser().username) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": 'Cannot delete yourself!',
|
||||||
|
"title": "Cannot delete user",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.userToDelete = user;
|
||||||
|
$('#confirmDeleteUserModal').modal({});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeUserPassword = function(user) {
|
||||||
|
$('#changePasswordModal').modal('hide');
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'username': user.username
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'password': user.password
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||||
|
$scope.loadUsersInternal();
|
||||||
|
}, function(resp) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": resp.data ? resp.data.message : 'Could not change user',
|
||||||
|
"title": "Cannot change user",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteUser = function(user) {
|
||||||
|
$('#confirmDeleteUserModal').modal('hide');
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'username': user.username
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||||
|
$scope.loadUsersInternal();
|
||||||
|
}, function(resp) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": resp.data ? resp.data.message : 'Could not delete user',
|
||||||
|
"title": "Cannot delete user",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var seatUsageLoaded = function(usage) {
|
||||||
|
$scope.usageLoading = false;
|
||||||
|
|
||||||
|
if (usage.count > usage.allowed) {
|
||||||
|
$scope.limit = 'over';
|
||||||
|
} else if (usage.count == usage.allowed) {
|
||||||
|
$scope.limit = 'at';
|
||||||
|
} else if (usage.count >= usage.allowed * 0.7) {
|
||||||
|
$scope.limit = 'near';
|
||||||
|
} else {
|
||||||
|
$scope.limit = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.chart) {
|
||||||
|
$scope.chart = new UsageChart();
|
||||||
|
$scope.chart.draw('seat-usage-chart');
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.chart.update(usage.count, usage.allowed);
|
||||||
|
};
|
||||||
|
|
||||||
|
var loadSeatUsage = function() {
|
||||||
|
$scope.usageLoading = true;
|
||||||
|
ApiService.getSeatCount().then(function(resp) {
|
||||||
|
seatUsageLoaded(resp);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSeatUsage();
|
||||||
}
|
}
|
|
@ -115,12 +115,20 @@ ImageHistoryTree.prototype.setupOverscroll_ = function() {
|
||||||
$(that).trigger({
|
$(that).trigger({
|
||||||
'type': 'hideTagMenu'
|
'type': 'hideTagMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideImageMenu'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
overscroll.on('scroll', function() {
|
overscroll.on('scroll', function() {
|
||||||
$(that).trigger({
|
$(that).trigger({
|
||||||
'type': 'hideTagMenu'
|
'type': 'hideTagMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideImageMenu'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -178,6 +186,11 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
// Save the container.
|
// Save the container.
|
||||||
this.container_ = container;
|
this.container_ = container;
|
||||||
|
|
||||||
|
if (!$('#' + container)[0]) {
|
||||||
|
this.container_ = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the tree and all its components.
|
// Create the tree and all its components.
|
||||||
var tree = d3.layout.tree()
|
var tree = d3.layout.tree()
|
||||||
.separation(function() { return 2; });
|
.separation(function() { return 2; });
|
||||||
|
@ -185,11 +198,10 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
var diagonal = d3.svg.diagonal()
|
var diagonal = d3.svg.diagonal()
|
||||||
.projection(function(d) { return [d.x, d.y]; });
|
.projection(function(d) { return [d.x, d.y]; });
|
||||||
|
|
||||||
var rootSvg = d3.select("#" + container).append("svg:svg")
|
var rootSvg = d3.select("#" + container).append("svg:svg")
|
||||||
.attr("class", "image-tree");
|
.attr("class", "image-tree");
|
||||||
|
|
||||||
var vis = rootSvg.append("svg:g");
|
var vis = rootSvg.append("svg:g");
|
||||||
|
|
||||||
var formatComment = this.formatComment_;
|
var formatComment = this.formatComment_;
|
||||||
var formatTime = this.formatTime_;
|
var formatTime = this.formatTime_;
|
||||||
var formatCommand = this.formatCommand_;
|
var formatCommand = this.formatCommand_;
|
||||||
|
@ -254,6 +266,8 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
|
||||||
this.setTag_(this.currentTag_);
|
this.setTag_(this.currentTag_);
|
||||||
this.setupOverscroll_();
|
this.setupOverscroll_();
|
||||||
|
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -664,7 +678,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (d.collapsed) { that.expandCollapsed_(d); }
|
if (d.collapsed) { that.expandCollapsed_(d); }
|
||||||
})
|
})
|
||||||
.on('mouseover', tip.show)
|
.on('mouseover', tip.show)
|
||||||
.on('mouseout', tip.hide);
|
.on('mouseout', tip.hide)
|
||||||
|
.on("contextmenu", function(d, e) {
|
||||||
|
d3.event.preventDefault();
|
||||||
|
|
||||||
|
if (d.image) {
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'showImageMenu',
|
||||||
|
'image': d.image.id,
|
||||||
|
'clientX': d3.event.clientX,
|
||||||
|
'clientY': d3.event.clientY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
nodeEnter.selectAll("tags")
|
nodeEnter.selectAll("tags")
|
||||||
.append("svg:text")
|
.append("svg:text")
|
||||||
|
@ -732,15 +758,16 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = '';
|
var html = '<div style="width: ' + DEPTH_HEIGHT + 'px">';
|
||||||
for (var i = 0; i < d.tags.length; ++i) {
|
for (var i = 0; i < d.tags.length; ++i) {
|
||||||
var tag = d.tags[i];
|
var tag = d.tags[i];
|
||||||
var kind = 'default';
|
var kind = 'default';
|
||||||
if (tag == currentTag) {
|
if (tag == currentTag) {
|
||||||
kind = 'success';
|
kind = 'success';
|
||||||
}
|
}
|
||||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '">' + tag + '</span>';
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -909,6 +936,8 @@ FileTreeBase.prototype.calculateDimensions_ = function(container) {
|
||||||
* Updates the dimensions of the tree.
|
* Updates the dimensions of the tree.
|
||||||
*/
|
*/
|
||||||
FileTreeBase.prototype.updateDimensions_ = function() {
|
FileTreeBase.prototype.updateDimensions_ = function() {
|
||||||
|
if (!this.rootSvg_) { return; }
|
||||||
|
|
||||||
var container = this.container_;
|
var container = this.container_;
|
||||||
var dimensions = this.calculateDimensions_(container);
|
var dimensions = this.calculateDimensions_(container);
|
||||||
|
|
||||||
|
@ -1106,7 +1135,12 @@ FileTreeBase.prototype.update_ = function(source) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the height of the container and the SVG.
|
// Update the height of the container and the SVG.
|
||||||
document.getElementById(this.container_).style.height = this.getContainerHeight_() + 'px';
|
var containerElm = document.getElementById(this.container_);
|
||||||
|
if (!containerElm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
containerElm.style.height = this.getContainerHeight_() + 'px';
|
||||||
svg.attr('height', this.getContainerHeight_());
|
svg.attr('height', this.getContainerHeight_());
|
||||||
|
|
||||||
// Compute the flattened node list.
|
// Compute the flattened node list.
|
||||||
|
@ -1359,7 +1393,7 @@ FileTree.prototype.getNodesHeight = function() {
|
||||||
/**
|
/**
|
||||||
* Based off of http://bl.ocks.org/mbostock/1346410
|
* Based off of http://bl.ocks.org/mbostock/1346410
|
||||||
*/
|
*/
|
||||||
function RepositoryUsageChart() {
|
function UsageChart() {
|
||||||
this.total_ = null;
|
this.total_ = null;
|
||||||
this.count_ = null;
|
this.count_ = null;
|
||||||
this.drawn_ = false;
|
this.drawn_ = false;
|
||||||
|
@ -1369,7 +1403,7 @@ function RepositoryUsageChart() {
|
||||||
/**
|
/**
|
||||||
* Updates the chart with the given count and total of number of repositories.
|
* Updates the chart with the given count and total of number of repositories.
|
||||||
*/
|
*/
|
||||||
RepositoryUsageChart.prototype.update = function(count, total) {
|
UsageChart.prototype.update = function(count, total) {
|
||||||
if (!this.g_) { return; }
|
if (!this.g_) { return; }
|
||||||
this.total_ = total;
|
this.total_ = total;
|
||||||
this.count_ = count;
|
this.count_ = count;
|
||||||
|
@ -1380,7 +1414,7 @@ RepositoryUsageChart.prototype.update = function(count, total) {
|
||||||
/**
|
/**
|
||||||
* Conducts the actual draw or update (if applicable).
|
* Conducts the actual draw or update (if applicable).
|
||||||
*/
|
*/
|
||||||
RepositoryUsageChart.prototype.drawInternal_ = function() {
|
UsageChart.prototype.drawInternal_ = function() {
|
||||||
// If the total is null, then we have not yet set the proper counts.
|
// If the total is null, then we have not yet set the proper counts.
|
||||||
if (this.total_ === null) { return; }
|
if (this.total_ === null) { return; }
|
||||||
|
|
||||||
|
@ -1439,7 +1473,7 @@ RepositoryUsageChart.prototype.drawInternal_ = function() {
|
||||||
/**
|
/**
|
||||||
* Draws the chart in the given container.
|
* Draws the chart in the given container.
|
||||||
*/
|
*/
|
||||||
RepositoryUsageChart.prototype.draw = function(container) {
|
UsageChart.prototype.draw = function(container) {
|
||||||
var cw = 200;
|
var cw = 200;
|
||||||
var ch = 200;
|
var ch = 200;
|
||||||
var radius = Math.min(cw, ch) / 2;
|
var radius = Math.min(cw, ch) / 2;
|
||||||
|
@ -1668,7 +1702,12 @@ LogUsageChart.prototype.handleStateChange_ = function(e) {
|
||||||
*/
|
*/
|
||||||
LogUsageChart.prototype.draw = function(container, logData, startDate, endDate) {
|
LogUsageChart.prototype.draw = function(container, logData, startDate, endDate) {
|
||||||
// Reset the container's contents.
|
// Reset the container's contents.
|
||||||
document.getElementById(container).innerHTML = '<svg></svg>';
|
var containerElm = document.getElementById(container);
|
||||||
|
if (!containerElm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
containerElm.innerHTML = '<svg></svg>';
|
||||||
|
|
||||||
// Returns a date offset from the given date by "days" Days.
|
// Returns a date offset from the given date by "days" Days.
|
||||||
var offsetDate = function(d, days) {
|
var offsetDate = function(d, days) {
|
||||||
|
@ -1716,7 +1755,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate)
|
||||||
.duration(500)
|
.duration(500)
|
||||||
.call(chart);
|
.call(chart);
|
||||||
|
|
||||||
nv.utils.windoweResize(chart.update);
|
nv.utils.windowResize(chart.update);
|
||||||
|
|
||||||
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
||||||
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
||||||
|
|
|
@ -135,7 +135,7 @@ angular.module("angular-tour", [])
|
||||||
};
|
};
|
||||||
|
|
||||||
var fireMixpanelEvent = function() {
|
var fireMixpanelEvent = function() {
|
||||||
if (!$scope.step || !mixpanel) { return; }
|
if (!$scope.step || !window['mixpanel']) { return; }
|
||||||
|
|
||||||
var eventName = $scope.step['mixpanelEvent'];
|
var eventName = $scope.step['mixpanelEvent'];
|
||||||
if (eventName) {
|
if (eventName) {
|
||||||
|
|
|
@ -230,3 +230,4 @@ var saveAs = saveAs
|
||||||
// with an attribute `content` that corresponds to the window
|
// with an attribute `content` that corresponds to the window
|
||||||
|
|
||||||
if (typeof module !== 'undefined') module.exports = saveAs;
|
if (typeof module !== 'undefined') module.exports = saveAs;
|
||||||
|
window.saveAs = saveAs;
|
3543
static/lib/angular-strap.js
vendored
3543
static/lib/angular-strap.js
vendored
File diff suppressed because it is too large
Load diff
9275
static/lib/d3.js
vendored
9275
static/lib/d3.js
vendored
File diff suppressed because it is too large
Load diff
5
static/lib/d3.v3.min.js
vendored
5
static/lib/d3.v3.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,790 +0,0 @@
|
||||||
/*!
|
|
||||||
* Datepicker for Bootstrap
|
|
||||||
*
|
|
||||||
* Copyright 2012 Stefan Petre
|
|
||||||
* Improvements by Andrew Rowls
|
|
||||||
* Licensed under the Apache License v2.0
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
.datepicker {
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
direction: ltr;
|
|
||||||
/*.dow {
|
|
||||||
border-top: 1px solid #ddd !important;
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
.datepicker-inline {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
.datepicker.datepicker-rtl {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
.datepicker.datepicker-rtl table tr td span {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown:before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
border-bottom: 7px solid #ccc;
|
|
||||||
border-top: 0;
|
|
||||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown:after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid #fff;
|
|
||||||
border-top: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-left:before {
|
|
||||||
left: 6px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-left:after {
|
|
||||||
left: 7px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-right:before {
|
|
||||||
right: 6px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-right:after {
|
|
||||||
right: 7px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-top:before {
|
|
||||||
top: -7px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-top:after {
|
|
||||||
top: -6px;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-bottom:before {
|
|
||||||
bottom: -7px;
|
|
||||||
border-bottom: 0;
|
|
||||||
border-top: 7px solid #999;
|
|
||||||
}
|
|
||||||
.datepicker-dropdown.datepicker-orient-bottom:after {
|
|
||||||
bottom: -6px;
|
|
||||||
border-bottom: 0;
|
|
||||||
border-top: 6px solid #fff;
|
|
||||||
}
|
|
||||||
.datepicker > div {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.datepicker.days div.datepicker-days {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.datepicker.months div.datepicker-months {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.datepicker.years div.datepicker-years {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.datepicker table {
|
|
||||||
margin: 0;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td,
|
|
||||||
.datepicker table tr th {
|
|
||||||
text-align: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.table-striped .datepicker table tr td,
|
|
||||||
.table-striped .datepicker table tr th {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.day:hover,
|
|
||||||
.datepicker table tr td.day.focused {
|
|
||||||
background: #eeeeee;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.old,
|
|
||||||
.datepicker table tr td.new {
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.disabled,
|
|
||||||
.datepicker table tr td.disabled:hover {
|
|
||||||
background: none;
|
|
||||||
color: #999999;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today,
|
|
||||||
.datepicker table tr td.today:hover,
|
|
||||||
.datepicker table tr td.today.disabled,
|
|
||||||
.datepicker table tr td.today.disabled:hover {
|
|
||||||
color: #000000;
|
|
||||||
background-color: #ffdb99;
|
|
||||||
border-color: #ffb733;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today:hover,
|
|
||||||
.datepicker table tr td.today:hover:hover,
|
|
||||||
.datepicker table tr td.today.disabled:hover,
|
|
||||||
.datepicker table tr td.today.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.today:focus,
|
|
||||||
.datepicker table tr td.today:hover:focus,
|
|
||||||
.datepicker table tr td.today.disabled:focus,
|
|
||||||
.datepicker table tr td.today.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.today:active,
|
|
||||||
.datepicker table tr td.today:hover:active,
|
|
||||||
.datepicker table tr td.today.disabled:active,
|
|
||||||
.datepicker table tr td.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.today.active,
|
|
||||||
.datepicker table tr td.today:hover.active,
|
|
||||||
.datepicker table tr td.today.disabled.active,
|
|
||||||
.datepicker table tr td.today.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
|
|
||||||
color: #000000;
|
|
||||||
background-color: #ffcd70;
|
|
||||||
border-color: #f59e00;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today:active,
|
|
||||||
.datepicker table tr td.today:hover:active,
|
|
||||||
.datepicker table tr td.today.disabled:active,
|
|
||||||
.datepicker table tr td.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.today.active,
|
|
||||||
.datepicker table tr td.today:hover.active,
|
|
||||||
.datepicker table tr td.today.disabled.active,
|
|
||||||
.datepicker table tr td.today.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today.disabled,
|
|
||||||
.datepicker table tr td.today:hover.disabled,
|
|
||||||
.datepicker table tr td.today.disabled.disabled,
|
|
||||||
.datepicker table tr td.today.disabled:hover.disabled,
|
|
||||||
.datepicker table tr td.today[disabled],
|
|
||||||
.datepicker table tr td.today:hover[disabled],
|
|
||||||
.datepicker table tr td.today.disabled[disabled],
|
|
||||||
.datepicker table tr td.today.disabled:hover[disabled],
|
|
||||||
fieldset[disabled] .datepicker table tr td.today,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
|
|
||||||
.datepicker table tr td.today.disabled:hover,
|
|
||||||
.datepicker table tr td.today:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.today.disabled.disabled:hover,
|
|
||||||
.datepicker table tr td.today.disabled:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.today[disabled]:hover,
|
|
||||||
.datepicker table tr td.today:hover[disabled]:hover,
|
|
||||||
.datepicker table tr td.today.disabled[disabled]:hover,
|
|
||||||
.datepicker table tr td.today.disabled:hover[disabled]:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.today.disabled:focus,
|
|
||||||
.datepicker table tr td.today:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.today.disabled.disabled:focus,
|
|
||||||
.datepicker table tr td.today.disabled:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.today[disabled]:focus,
|
|
||||||
.datepicker table tr td.today:hover[disabled]:focus,
|
|
||||||
.datepicker table tr td.today.disabled[disabled]:focus,
|
|
||||||
.datepicker table tr td.today.disabled:hover[disabled]:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.today.disabled:active,
|
|
||||||
.datepicker table tr td.today:hover.disabled:active,
|
|
||||||
.datepicker table tr td.today.disabled.disabled:active,
|
|
||||||
.datepicker table tr td.today.disabled:hover.disabled:active,
|
|
||||||
.datepicker table tr td.today[disabled]:active,
|
|
||||||
.datepicker table tr td.today:hover[disabled]:active,
|
|
||||||
.datepicker table tr td.today.disabled[disabled]:active,
|
|
||||||
.datepicker table tr td.today.disabled:hover[disabled]:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.today.disabled.active,
|
|
||||||
.datepicker table tr td.today:hover.disabled.active,
|
|
||||||
.datepicker table tr td.today.disabled.disabled.active,
|
|
||||||
.datepicker table tr td.today.disabled:hover.disabled.active,
|
|
||||||
.datepicker table tr td.today[disabled].active,
|
|
||||||
.datepicker table tr td.today:hover[disabled].active,
|
|
||||||
.datepicker table tr td.today.disabled[disabled].active,
|
|
||||||
.datepicker table tr td.today.disabled:hover[disabled].active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today:hover.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover.active {
|
|
||||||
background-color: #ffdb99;
|
|
||||||
border-color: #ffb733;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today:hover:hover {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.today.active:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.range,
|
|
||||||
.datepicker table tr td.range:hover,
|
|
||||||
.datepicker table tr td.range.disabled,
|
|
||||||
.datepicker table tr td.range.disabled:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.range.today,
|
|
||||||
.datepicker table tr td.range.today:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover {
|
|
||||||
color: #000000;
|
|
||||||
background-color: #f7ca77;
|
|
||||||
border-color: #f1a417;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.range.today:hover,
|
|
||||||
.datepicker table tr td.range.today:hover:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.range.today:focus,
|
|
||||||
.datepicker table tr td.range.today:hover:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.range.today:active,
|
|
||||||
.datepicker table tr td.range.today:hover:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.range.today.active,
|
|
||||||
.datepicker table tr td.range.today:hover.active,
|
|
||||||
.datepicker table tr td.range.today.disabled.active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
|
|
||||||
color: #000000;
|
|
||||||
background-color: #f4bb51;
|
|
||||||
border-color: #bf800c;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.range.today:active,
|
|
||||||
.datepicker table tr td.range.today:hover:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.range.today.active,
|
|
||||||
.datepicker table tr td.range.today:hover.active,
|
|
||||||
.datepicker table tr td.range.today.disabled.active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.range.today.disabled,
|
|
||||||
.datepicker table tr td.range.today:hover.disabled,
|
|
||||||
.datepicker table tr td.range.today.disabled.disabled,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.disabled,
|
|
||||||
.datepicker table tr td.range.today[disabled],
|
|
||||||
.datepicker table tr td.range.today:hover[disabled],
|
|
||||||
.datepicker table tr td.range.today.disabled[disabled],
|
|
||||||
.datepicker table tr td.range.today.disabled:hover[disabled],
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.range.today[disabled]:hover,
|
|
||||||
.datepicker table tr td.range.today:hover[disabled]:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled[disabled]:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover[disabled]:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.range.today.disabled:focus,
|
|
||||||
.datepicker table tr td.range.today:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled.disabled:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.range.today[disabled]:focus,
|
|
||||||
.datepicker table tr td.range.today:hover[disabled]:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled[disabled]:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover[disabled]:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.range.today.disabled:active,
|
|
||||||
.datepicker table tr td.range.today:hover.disabled:active,
|
|
||||||
.datepicker table tr td.range.today.disabled.disabled:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.disabled:active,
|
|
||||||
.datepicker table tr td.range.today[disabled]:active,
|
|
||||||
.datepicker table tr td.range.today:hover[disabled]:active,
|
|
||||||
.datepicker table tr td.range.today.disabled[disabled]:active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover[disabled]:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:active,
|
|
||||||
.datepicker table tr td.range.today.disabled.active,
|
|
||||||
.datepicker table tr td.range.today:hover.disabled.active,
|
|
||||||
.datepicker table tr td.range.today.disabled.disabled.active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover.disabled.active,
|
|
||||||
.datepicker table tr td.range.today[disabled].active,
|
|
||||||
.datepicker table tr td.range.today:hover[disabled].active,
|
|
||||||
.datepicker table tr td.range.today.disabled[disabled].active,
|
|
||||||
.datepicker table tr td.range.today.disabled:hover[disabled].active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today:hover.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover.active {
|
|
||||||
background-color: #f7ca77;
|
|
||||||
border-color: #f1a417;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.selected,
|
|
||||||
.datepicker table tr td.selected:hover,
|
|
||||||
.datepicker table tr td.selected.disabled,
|
|
||||||
.datepicker table tr td.selected.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #999999;
|
|
||||||
border-color: #555555;
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.datepicker table tr td.selected:hover,
|
|
||||||
.datepicker table tr td.selected:hover:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.selected:focus,
|
|
||||||
.datepicker table tr td.selected:hover:focus,
|
|
||||||
.datepicker table tr td.selected.disabled:focus,
|
|
||||||
.datepicker table tr td.selected.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.selected:active,
|
|
||||||
.datepicker table tr td.selected:hover:active,
|
|
||||||
.datepicker table tr td.selected.disabled:active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover:active,
|
|
||||||
.datepicker table tr td.selected.active,
|
|
||||||
.datepicker table tr td.selected:hover.active,
|
|
||||||
.datepicker table tr td.selected.disabled.active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #858585;
|
|
||||||
border-color: #373737;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.selected:active,
|
|
||||||
.datepicker table tr td.selected:hover:active,
|
|
||||||
.datepicker table tr td.selected.disabled:active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover:active,
|
|
||||||
.datepicker table tr td.selected.active,
|
|
||||||
.datepicker table tr td.selected:hover.active,
|
|
||||||
.datepicker table tr td.selected.disabled.active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.selected.disabled,
|
|
||||||
.datepicker table tr td.selected:hover.disabled,
|
|
||||||
.datepicker table tr td.selected.disabled.disabled,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.disabled,
|
|
||||||
.datepicker table tr td.selected[disabled],
|
|
||||||
.datepicker table tr td.selected:hover[disabled],
|
|
||||||
.datepicker table tr td.selected.disabled[disabled],
|
|
||||||
.datepicker table tr td.selected.disabled:hover[disabled],
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:hover,
|
|
||||||
.datepicker table tr td.selected:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.selected.disabled.disabled:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.selected[disabled]:hover,
|
|
||||||
.datepicker table tr td.selected:hover[disabled]:hover,
|
|
||||||
.datepicker table tr td.selected.disabled[disabled]:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:hover[disabled]:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.selected.disabled:focus,
|
|
||||||
.datepicker table tr td.selected:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.selected.disabled.disabled:focus,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.selected[disabled]:focus,
|
|
||||||
.datepicker table tr td.selected:hover[disabled]:focus,
|
|
||||||
.datepicker table tr td.selected.disabled[disabled]:focus,
|
|
||||||
.datepicker table tr td.selected.disabled:hover[disabled]:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.selected.disabled:active,
|
|
||||||
.datepicker table tr td.selected:hover.disabled:active,
|
|
||||||
.datepicker table tr td.selected.disabled.disabled:active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.disabled:active,
|
|
||||||
.datepicker table tr td.selected[disabled]:active,
|
|
||||||
.datepicker table tr td.selected:hover[disabled]:active,
|
|
||||||
.datepicker table tr td.selected.disabled[disabled]:active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover[disabled]:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:active,
|
|
||||||
.datepicker table tr td.selected.disabled.active,
|
|
||||||
.datepicker table tr td.selected:hover.disabled.active,
|
|
||||||
.datepicker table tr td.selected.disabled.disabled.active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover.disabled.active,
|
|
||||||
.datepicker table tr td.selected[disabled].active,
|
|
||||||
.datepicker table tr td.selected:hover[disabled].active,
|
|
||||||
.datepicker table tr td.selected.disabled[disabled].active,
|
|
||||||
.datepicker table tr td.selected.disabled:hover[disabled].active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected:hover.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover.active {
|
|
||||||
background-color: #999999;
|
|
||||||
border-color: #555555;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.active,
|
|
||||||
.datepicker table tr td.active:hover,
|
|
||||||
.datepicker table tr td.active.disabled,
|
|
||||||
.datepicker table tr td.active.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #428bca;
|
|
||||||
border-color: #357ebd;
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.datepicker table tr td.active:hover,
|
|
||||||
.datepicker table tr td.active:hover:hover,
|
|
||||||
.datepicker table tr td.active.disabled:hover,
|
|
||||||
.datepicker table tr td.active.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.active:focus,
|
|
||||||
.datepicker table tr td.active:hover:focus,
|
|
||||||
.datepicker table tr td.active.disabled:focus,
|
|
||||||
.datepicker table tr td.active.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.active:active,
|
|
||||||
.datepicker table tr td.active:hover:active,
|
|
||||||
.datepicker table tr td.active.disabled:active,
|
|
||||||
.datepicker table tr td.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td.active.active,
|
|
||||||
.datepicker table tr td.active:hover.active,
|
|
||||||
.datepicker table tr td.active.disabled.active,
|
|
||||||
.datepicker table tr td.active.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #3276b1;
|
|
||||||
border-color: #285e8e;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.active:active,
|
|
||||||
.datepicker table tr td.active:hover:active,
|
|
||||||
.datepicker table tr td.active.disabled:active,
|
|
||||||
.datepicker table tr td.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td.active.active,
|
|
||||||
.datepicker table tr td.active:hover.active,
|
|
||||||
.datepicker table tr td.active.disabled.active,
|
|
||||||
.datepicker table tr td.active.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td.active.disabled,
|
|
||||||
.datepicker table tr td.active:hover.disabled,
|
|
||||||
.datepicker table tr td.active.disabled.disabled,
|
|
||||||
.datepicker table tr td.active.disabled:hover.disabled,
|
|
||||||
.datepicker table tr td.active[disabled],
|
|
||||||
.datepicker table tr td.active:hover[disabled],
|
|
||||||
.datepicker table tr td.active.disabled[disabled],
|
|
||||||
.datepicker table tr td.active.disabled:hover[disabled],
|
|
||||||
fieldset[disabled] .datepicker table tr td.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
|
|
||||||
.datepicker table tr td.active.disabled:hover,
|
|
||||||
.datepicker table tr td.active:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.active.disabled.disabled:hover,
|
|
||||||
.datepicker table tr td.active.disabled:hover.disabled:hover,
|
|
||||||
.datepicker table tr td.active[disabled]:hover,
|
|
||||||
.datepicker table tr td.active:hover[disabled]:hover,
|
|
||||||
.datepicker table tr td.active.disabled[disabled]:hover,
|
|
||||||
.datepicker table tr td.active.disabled:hover[disabled]:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:hover,
|
|
||||||
.datepicker table tr td.active.disabled:focus,
|
|
||||||
.datepicker table tr td.active:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.active.disabled.disabled:focus,
|
|
||||||
.datepicker table tr td.active.disabled:hover.disabled:focus,
|
|
||||||
.datepicker table tr td.active[disabled]:focus,
|
|
||||||
.datepicker table tr td.active:hover[disabled]:focus,
|
|
||||||
.datepicker table tr td.active.disabled[disabled]:focus,
|
|
||||||
.datepicker table tr td.active.disabled:hover[disabled]:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:focus,
|
|
||||||
.datepicker table tr td.active.disabled:active,
|
|
||||||
.datepicker table tr td.active:hover.disabled:active,
|
|
||||||
.datepicker table tr td.active.disabled.disabled:active,
|
|
||||||
.datepicker table tr td.active.disabled:hover.disabled:active,
|
|
||||||
.datepicker table tr td.active[disabled]:active,
|
|
||||||
.datepicker table tr td.active:hover[disabled]:active,
|
|
||||||
.datepicker table tr td.active.disabled[disabled]:active,
|
|
||||||
.datepicker table tr td.active.disabled:hover[disabled]:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td.active.disabled.active,
|
|
||||||
.datepicker table tr td.active:hover.disabled.active,
|
|
||||||
.datepicker table tr td.active.disabled.disabled.active,
|
|
||||||
.datepicker table tr td.active.disabled:hover.disabled.active,
|
|
||||||
.datepicker table tr td.active[disabled].active,
|
|
||||||
.datepicker table tr td.active:hover[disabled].active,
|
|
||||||
.datepicker table tr td.active.disabled[disabled].active,
|
|
||||||
.datepicker table tr td.active.disabled:hover[disabled].active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active:hover.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover.active {
|
|
||||||
background-color: #428bca;
|
|
||||||
border-color: #357ebd;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span {
|
|
||||||
display: block;
|
|
||||||
width: 23%;
|
|
||||||
height: 54px;
|
|
||||||
line-height: 54px;
|
|
||||||
float: left;
|
|
||||||
margin: 1%;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.disabled,
|
|
||||||
.datepicker table tr td span.disabled:hover {
|
|
||||||
background: none;
|
|
||||||
color: #999999;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.active,
|
|
||||||
.datepicker table tr td span.active:hover,
|
|
||||||
.datepicker table tr td span.active.disabled,
|
|
||||||
.datepicker table tr td span.active.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #428bca;
|
|
||||||
border-color: #357ebd;
|
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.active:hover,
|
|
||||||
.datepicker table tr td span.active:hover:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:hover:hover,
|
|
||||||
.datepicker table tr td span.active:focus,
|
|
||||||
.datepicker table tr td span.active:hover:focus,
|
|
||||||
.datepicker table tr td span.active.disabled:focus,
|
|
||||||
.datepicker table tr td span.active.disabled:hover:focus,
|
|
||||||
.datepicker table tr td span.active:active,
|
|
||||||
.datepicker table tr td span.active:hover:active,
|
|
||||||
.datepicker table tr td span.active.disabled:active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td span.active.active,
|
|
||||||
.datepicker table tr td span.active:hover.active,
|
|
||||||
.datepicker table tr td span.active.disabled.active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #3276b1;
|
|
||||||
border-color: #285e8e;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.active:active,
|
|
||||||
.datepicker table tr td span.active:hover:active,
|
|
||||||
.datepicker table tr td span.active.disabled:active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td span.active.active,
|
|
||||||
.datepicker table tr td span.active:hover.active,
|
|
||||||
.datepicker table tr td span.active.disabled.active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active:hover,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
|
|
||||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.active.disabled,
|
|
||||||
.datepicker table tr td span.active:hover.disabled,
|
|
||||||
.datepicker table tr td span.active.disabled.disabled,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.disabled,
|
|
||||||
.datepicker table tr td span.active[disabled],
|
|
||||||
.datepicker table tr td span.active:hover[disabled],
|
|
||||||
.datepicker table tr td span.active.disabled[disabled],
|
|
||||||
.datepicker table tr td span.active.disabled:hover[disabled],
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:hover,
|
|
||||||
.datepicker table tr td span.active:hover.disabled:hover,
|
|
||||||
.datepicker table tr td span.active.disabled.disabled:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.disabled:hover,
|
|
||||||
.datepicker table tr td span.active[disabled]:hover,
|
|
||||||
.datepicker table tr td span.active:hover[disabled]:hover,
|
|
||||||
.datepicker table tr td span.active.disabled[disabled]:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
|
|
||||||
.datepicker table tr td span.active.disabled:focus,
|
|
||||||
.datepicker table tr td span.active:hover.disabled:focus,
|
|
||||||
.datepicker table tr td span.active.disabled.disabled:focus,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.disabled:focus,
|
|
||||||
.datepicker table tr td span.active[disabled]:focus,
|
|
||||||
.datepicker table tr td span.active:hover[disabled]:focus,
|
|
||||||
.datepicker table tr td span.active.disabled[disabled]:focus,
|
|
||||||
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
|
|
||||||
.datepicker table tr td span.active.disabled:active,
|
|
||||||
.datepicker table tr td span.active:hover.disabled:active,
|
|
||||||
.datepicker table tr td span.active.disabled.disabled:active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.disabled:active,
|
|
||||||
.datepicker table tr td span.active[disabled]:active,
|
|
||||||
.datepicker table tr td span.active:hover[disabled]:active,
|
|
||||||
.datepicker table tr td span.active.disabled[disabled]:active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover[disabled]:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:active,
|
|
||||||
.datepicker table tr td span.active.disabled.active,
|
|
||||||
.datepicker table tr td span.active:hover.disabled.active,
|
|
||||||
.datepicker table tr td span.active.disabled.disabled.active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover.disabled.active,
|
|
||||||
.datepicker table tr td span.active[disabled].active,
|
|
||||||
.datepicker table tr td span.active:hover[disabled].active,
|
|
||||||
.datepicker table tr td span.active.disabled[disabled].active,
|
|
||||||
.datepicker table tr td span.active.disabled:hover[disabled].active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active:hover.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled.active,
|
|
||||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.active {
|
|
||||||
background-color: #428bca;
|
|
||||||
border-color: #357ebd;
|
|
||||||
}
|
|
||||||
.datepicker table tr td span.old,
|
|
||||||
.datepicker table tr td span.new {
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
.datepicker th.datepicker-switch {
|
|
||||||
width: 145px;
|
|
||||||
}
|
|
||||||
.datepicker thead tr:first-child th,
|
|
||||||
.datepicker tfoot tr th {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.datepicker thead tr:first-child th:hover,
|
|
||||||
.datepicker tfoot tr th:hover {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
.datepicker .cw {
|
|
||||||
font-size: 10px;
|
|
||||||
width: 12px;
|
|
||||||
padding: 0 2px 0 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.datepicker thead tr:first-child th.cw {
|
|
||||||
cursor: default;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.input-group.date .input-group-addon i {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
.input-daterange input {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.input-daterange input:first-child {
|
|
||||||
border-radius: 3px 0 0 3px;
|
|
||||||
}
|
|
||||||
.input-daterange input:last-child {
|
|
||||||
border-radius: 0 3px 3px 0;
|
|
||||||
}
|
|
||||||
.input-daterange .input-group-addon {
|
|
||||||
width: auto;
|
|
||||||
min-width: 16px;
|
|
||||||
padding: 4px 5px;
|
|
||||||
font-weight: normal;
|
|
||||||
line-height: 1.428571429;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 1px 0 #fff;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: #eeeeee;
|
|
||||||
border: solid #cccccc;
|
|
||||||
border-width: 1px 0;
|
|
||||||
margin-left: -5px;
|
|
||||||
margin-right: -5px;
|
|
||||||
}
|
|
||||||
.datepicker.dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
float: left;
|
|
||||||
display: none;
|
|
||||||
min-width: 160px;
|
|
||||||
list-style: none;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 5px;
|
|
||||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
-webkit-background-clip: padding-box;
|
|
||||||
-moz-background-clip: padding;
|
|
||||||
background-clip: padding-box;
|
|
||||||
*border-right-width: 2px;
|
|
||||||
*border-bottom-width: 2px;
|
|
||||||
color: #333333;
|
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.428571429;
|
|
||||||
}
|
|
||||||
.datepicker.dropdown-menu th,
|
|
||||||
.datepicker.dropdown-menu td {
|
|
||||||
padding: 4px 5px;
|
|
||||||
}
|
|
|
@ -1,793 +0,0 @@
|
||||||
/**
|
|
||||||
* Overscroll v1.7.3
|
|
||||||
* A jQuery Plugin that emulates the iPhone scrolling experience in a browser.
|
|
||||||
* http://azoffdesign.com/overscroll
|
|
||||||
*
|
|
||||||
* Intended for use with the latest jQuery
|
|
||||||
* http://code.jquery.com/jquery-latest.js
|
|
||||||
*
|
|
||||||
* Copyright 2013, Jonathan Azoff
|
|
||||||
* Licensed under the MIT license.
|
|
||||||
* https://github.com/azoff/overscroll/blob/master/mit.license
|
|
||||||
*
|
|
||||||
* For API documentation, see the README file
|
|
||||||
* http://azof.fr/pYCzuM
|
|
||||||
*
|
|
||||||
* Date: Tuesday, March 18th 2013
|
|
||||||
*/
|
|
||||||
(function(global, dom, browser, math, wait, cancel, namespace, $, none){
|
|
||||||
|
|
||||||
// We want to run this plug-in in strict-mode
|
|
||||||
// so that we may benefit from its optimizations
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// The key used to bind-instance specific data to an object
|
|
||||||
var datakey = 'overscroll';
|
|
||||||
|
|
||||||
// create <body> node if there's not one present (e.g., for test runners)
|
|
||||||
if (dom.body === null) {
|
|
||||||
dom.documentElement.appendChild(
|
|
||||||
dom.createElement('body')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// quick fix for IE 8 and below since getComputedStyle() is not supported
|
|
||||||
// TODO: find a better solution
|
|
||||||
if (!global.getComputedStyle) {
|
|
||||||
global.getComputedStyle = function (el, pseudo) {
|
|
||||||
this.el = el;
|
|
||||||
this.getPropertyValue = function (prop) {
|
|
||||||
var re = /(\-([a-z]){1})/g;
|
|
||||||
if (prop == 'float') prop = 'styleFloat';
|
|
||||||
if (re.test(prop)) {
|
|
||||||
prop = prop.replace(re, function () {
|
|
||||||
return arguments[2].toUpperCase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return el.currentStyle[prop] ? el.currentStyle[prop] : null;
|
|
||||||
};
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// runs feature detection for overscroll
|
|
||||||
var compat = {
|
|
||||||
animate: (function(){
|
|
||||||
var fn = global.requestAnimationFrame ||
|
|
||||||
global.webkitRequestAnimationFrame ||
|
|
||||||
global.mozRequestAnimationFrame ||
|
|
||||||
global.oRequestAnimationFrame ||
|
|
||||||
global.msRequestAnimationFrame ||
|
|
||||||
function(callback) { wait(callback, 1000/60); };
|
|
||||||
return function(callback) {
|
|
||||||
fn.call(global, callback);
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
overflowScrolling: (function(){
|
|
||||||
var style = '';
|
|
||||||
var div = dom.createElement('div');
|
|
||||||
var prefixes = ['webkit', 'moz', 'o', 'ms'];
|
|
||||||
dom.body.appendChild(div);
|
|
||||||
$.each(prefixes, function(i, prefix){
|
|
||||||
div.style[prefix + 'OverflowScrolling'] = 'touch';
|
|
||||||
});
|
|
||||||
div.style.overflowScrolling = 'touch';
|
|
||||||
var computedStyle = global.getComputedStyle(div);
|
|
||||||
if (!!computedStyle.overflowScrolling) {
|
|
||||||
style = 'overflow-scrolling';
|
|
||||||
} else {
|
|
||||||
$.each(prefixes, function(i, prefix){
|
|
||||||
if (!!computedStyle[prefix + 'OverflowScrolling']) {
|
|
||||||
style = '-' + prefix + '-overflow-scrolling';
|
|
||||||
}
|
|
||||||
return !style;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
div.parentNode.removeChild(div);
|
|
||||||
return style;
|
|
||||||
})(),
|
|
||||||
cursor: (function() {
|
|
||||||
var div = dom.createElement('div');
|
|
||||||
var prefixes = ['webkit', 'moz'];
|
|
||||||
var gmail = 'https://mail.google.com/mail/images/2/';
|
|
||||||
var style = {
|
|
||||||
grab: 'url('+gmail+'openhand.cur), move',
|
|
||||||
grabbing: 'url('+gmail+'closedhand.cur), move'
|
|
||||||
};
|
|
||||||
dom.body.appendChild(div);
|
|
||||||
$.each(prefixes, function(i, prefix){
|
|
||||||
var found, cursor = '-' + prefix + '-grab';
|
|
||||||
div.style.cursor = cursor;
|
|
||||||
var computedStyle = global.getComputedStyle(div);
|
|
||||||
found = computedStyle.cursor === cursor;
|
|
||||||
if (found) {
|
|
||||||
style = {
|
|
||||||
grab: '-' + prefix + '-grab',
|
|
||||||
grabbing: '-' + prefix + '-grabbing'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return !found;
|
|
||||||
});
|
|
||||||
div.parentNode.removeChild(div);
|
|
||||||
return style;
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are all the events that could possibly
|
|
||||||
// be used by the plug-in
|
|
||||||
var events = {
|
|
||||||
drag: 'mousemove touchmove',
|
|
||||||
end: 'mouseup mouseleave click touchend touchcancel',
|
|
||||||
hover: 'mouseenter mouseleave',
|
|
||||||
ignored: 'select dragstart drag',
|
|
||||||
scroll: 'scroll',
|
|
||||||
start: 'mousedown touchstart',
|
|
||||||
wheel: 'mousewheel DOMMouseScroll'
|
|
||||||
};
|
|
||||||
|
|
||||||
// These settings are used to tweak drift settings
|
|
||||||
// for the plug-in
|
|
||||||
var settings = {
|
|
||||||
captureThreshold: 3,
|
|
||||||
driftDecay: 1.1,
|
|
||||||
driftSequences: 22,
|
|
||||||
driftTimeout: 100,
|
|
||||||
scrollDelta: 15,
|
|
||||||
thumbOpacity: 0.7,
|
|
||||||
thumbThickness: 6,
|
|
||||||
thumbTimeout: 400,
|
|
||||||
wheelDelta: 20,
|
|
||||||
wheelTicks: 120
|
|
||||||
};
|
|
||||||
|
|
||||||
// These defaults are used to complement any options
|
|
||||||
// passed into the plug-in entry point
|
|
||||||
var defaults = {
|
|
||||||
cancelOn: 'select,input,textarea',
|
|
||||||
direction: 'multi',
|
|
||||||
dragHold: false,
|
|
||||||
hoverThumbs: false,
|
|
||||||
scrollDelta: settings.scrollDelta,
|
|
||||||
showThumbs: true,
|
|
||||||
persistThumbs: false,
|
|
||||||
captureWheel: true,
|
|
||||||
wheelDelta: settings.wheelDelta,
|
|
||||||
wheelDirection: 'multi',
|
|
||||||
zIndex: 999,
|
|
||||||
ignoreSizing: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Triggers a DOM event on the overscrolled element.
|
|
||||||
// All events are namespaced under the overscroll name
|
|
||||||
function triggerEvent(event, target) {
|
|
||||||
target.trigger('overscroll:' + event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to return a timestamp
|
|
||||||
function time() {
|
|
||||||
return (new Date()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Captures the position from an event, modifies the properties
|
|
||||||
// of the second argument to persist the position, and then
|
|
||||||
// returns the modified object
|
|
||||||
function capturePosition(event, position, index) {
|
|
||||||
position.x = event.pageX;
|
|
||||||
position.y = event.pageY;
|
|
||||||
position.time = time();
|
|
||||||
position.index = index;
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to move the thumbs around an overscrolled element
|
|
||||||
function moveThumbs(thumbs, sizing, left, top) {
|
|
||||||
|
|
||||||
var ml, mt;
|
|
||||||
|
|
||||||
if (thumbs && thumbs.added) {
|
|
||||||
if (thumbs.horizontal) {
|
|
||||||
ml = left * (1 + sizing.container.width / sizing.container.scrollWidth);
|
|
||||||
mt = top + sizing.thumbs.horizontal.top;
|
|
||||||
thumbs.horizontal.css('margin', mt + 'px 0 0 ' + ml + 'px');
|
|
||||||
}
|
|
||||||
if (thumbs.vertical) {
|
|
||||||
ml = left + sizing.thumbs.vertical.left;
|
|
||||||
mt = top * (1 + sizing.container.height / sizing.container.scrollHeight);
|
|
||||||
thumbs.vertical.css('margin', mt + 'px 0 0 ' + ml + 'px');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to toggle the thumbs on and off
|
|
||||||
// of an overscrolled element
|
|
||||||
function toggleThumbs(thumbs, options, dragging) {
|
|
||||||
if (thumbs && thumbs.added && !options.persistThumbs) {
|
|
||||||
if (dragging) {
|
|
||||||
if (thumbs.vertical) {
|
|
||||||
thumbs.vertical.stop(true, true).fadeTo('fast', settings.thumbOpacity);
|
|
||||||
}
|
|
||||||
if (thumbs.horizontal) {
|
|
||||||
thumbs.horizontal.stop(true, true).fadeTo('fast', settings.thumbOpacity);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (thumbs.vertical) {
|
|
||||||
thumbs.vertical.fadeTo('fast', 0);
|
|
||||||
}
|
|
||||||
if (thumbs.horizontal) {
|
|
||||||
thumbs.horizontal.fadeTo('fast', 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defers click event listeners to after a mouseup event.
|
|
||||||
// Used to avoid unintentional clicks
|
|
||||||
function deferClick(target) {
|
|
||||||
var clicks, key = 'events';
|
|
||||||
var events = $._data ? $._data(target[0], key) : target.data(key);
|
|
||||||
if (events && events.click) {
|
|
||||||
clicks = events.click.slice();
|
|
||||||
target.off('click').one('click', function(){
|
|
||||||
$.each(clicks, function(i, click){
|
|
||||||
target.click(click);
|
|
||||||
}); return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggles thumbs on hover. This event is only triggered
|
|
||||||
// if the hoverThumbs option is set
|
|
||||||
function hover(event) {
|
|
||||||
var data = event.data,
|
|
||||||
thumbs = data.thumbs,
|
|
||||||
options = data.options,
|
|
||||||
dragging = event.type === 'mouseenter';
|
|
||||||
toggleThumbs(thumbs, options, dragging);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function is only ever used when the overscrolled element
|
|
||||||
// scrolled outside of the scope of this plugin.
|
|
||||||
function scroll(event) {
|
|
||||||
var data = event.data;
|
|
||||||
if (!data.flags.dragged) {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
moveThumbs(data.thumbs, data.sizing, this.scrollLeft, this.scrollTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles mouse wheel scroll events
|
|
||||||
function wheel(event) {
|
|
||||||
|
|
||||||
// prevent any default wheel behavior
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var data = event.data,
|
|
||||||
options = data.options,
|
|
||||||
sizing = data.sizing,
|
|
||||||
thumbs = data.thumbs,
|
|
||||||
dwheel = data.wheel,
|
|
||||||
flags = data.flags,
|
|
||||||
original = event.originalEvent,
|
|
||||||
delta = 0, deltaX = 0, deltaY = 0;
|
|
||||||
|
|
||||||
// stop any drifts
|
|
||||||
flags.drifting = false;
|
|
||||||
|
|
||||||
// normalize the wheel ticks
|
|
||||||
if (original.detail) {
|
|
||||||
delta = -original.detail;
|
|
||||||
if (original.detailX) {
|
|
||||||
deltaX = -original.detailX;
|
|
||||||
}
|
|
||||||
if (original.detailY) {
|
|
||||||
deltaY = -original.detailY;
|
|
||||||
}
|
|
||||||
} else if (original.wheelDelta) {
|
|
||||||
delta = original.wheelDelta / settings.wheelTicks;
|
|
||||||
if (original.wheelDeltaX) {
|
|
||||||
deltaX = original.wheelDeltaX / settings.wheelTicks;
|
|
||||||
}
|
|
||||||
if (original.wheelDeltaY) {
|
|
||||||
deltaY = original.wheelDeltaY / settings.wheelTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply a pixel delta to each tick
|
|
||||||
delta *= options.wheelDelta;
|
|
||||||
deltaX *= options.wheelDelta;
|
|
||||||
deltaY *= options.wheelDelta;
|
|
||||||
|
|
||||||
// initialize flags if this is the first tick
|
|
||||||
if (!dwheel) {
|
|
||||||
data.target.data(datakey).dragging = flags.dragging = true;
|
|
||||||
data.wheel = dwheel = { timeout: null };
|
|
||||||
toggleThumbs(thumbs, options, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// actually modify scroll offsets
|
|
||||||
if (options.wheelDirection === 'vertical'){
|
|
||||||
/*jshint validthis:true */
|
|
||||||
this.scrollTop -= delta;
|
|
||||||
} else if ( options.wheelDirection === 'horizontal') {
|
|
||||||
this.scrollLeft -= delta;
|
|
||||||
} else {
|
|
||||||
this.scrollLeft -= deltaX;
|
|
||||||
this.scrollTop -= deltaY || delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dwheel.timeout) { cancel(dwheel.timeout); }
|
|
||||||
|
|
||||||
moveThumbs(thumbs, sizing, this.scrollLeft, this.scrollTop);
|
|
||||||
|
|
||||||
dwheel.timeout = wait(function() {
|
|
||||||
data.target.data(datakey).dragging = flags.dragging = false;
|
|
||||||
toggleThumbs(thumbs, options, data.wheel = null);
|
|
||||||
}, settings.thumbTimeout);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates the current scroll offset during a mouse move
|
|
||||||
function drag(event) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var data = event.data,
|
|
||||||
touches = event.originalEvent.touches,
|
|
||||||
options = data.options,
|
|
||||||
sizing = data.sizing,
|
|
||||||
thumbs = data.thumbs,
|
|
||||||
position = data.position,
|
|
||||||
flags = data.flags,
|
|
||||||
target = data.target.get(0);
|
|
||||||
|
|
||||||
|
|
||||||
// correct page coordinates for touch devices
|
|
||||||
if (touches && touches.length) {
|
|
||||||
event = touches[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flags.dragged) {
|
|
||||||
toggleThumbs(thumbs, options, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
flags.dragged = true;
|
|
||||||
|
|
||||||
if (options.direction !== 'vertical') {
|
|
||||||
target.scrollLeft -= (event.pageX - position.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.options.direction !== 'horizontal') {
|
|
||||||
target.scrollTop -= (event.pageY - position.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
capturePosition(event, data.position);
|
|
||||||
|
|
||||||
if (--data.capture.index <= 0) {
|
|
||||||
data.target.data(datakey).dragging = flags.dragging = true;
|
|
||||||
capturePosition(event, data.capture, settings.captureThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// sends the overscrolled element into a drift
|
|
||||||
function drift(target, event, callback) {
|
|
||||||
|
|
||||||
var data = event.data, dx, dy, xMod, yMod,
|
|
||||||
capture = data.capture,
|
|
||||||
options = data.options,
|
|
||||||
sizing = data.sizing,
|
|
||||||
thumbs = data.thumbs,
|
|
||||||
elapsed = time() - capture.time,
|
|
||||||
scrollLeft = target.scrollLeft,
|
|
||||||
scrollTop = target.scrollTop,
|
|
||||||
decay = settings.driftDecay;
|
|
||||||
|
|
||||||
// only drift if enough time has passed since
|
|
||||||
// the last capture event
|
|
||||||
if (elapsed > settings.driftTimeout) {
|
|
||||||
callback(data); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine offset between last capture and current time
|
|
||||||
dx = options.scrollDelta * (event.pageX - capture.x);
|
|
||||||
dy = options.scrollDelta * (event.pageY - capture.y);
|
|
||||||
|
|
||||||
// update target scroll offsets
|
|
||||||
if (options.direction !== 'vertical') {
|
|
||||||
scrollLeft -= dx;
|
|
||||||
} if (options.direction !== 'horizontal') {
|
|
||||||
scrollTop -= dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// split the distance to travel into a set of sequences
|
|
||||||
xMod = dx / settings.driftSequences;
|
|
||||||
yMod = dy / settings.driftSequences;
|
|
||||||
|
|
||||||
triggerEvent('driftstart', data.target);
|
|
||||||
|
|
||||||
data.drifting = true;
|
|
||||||
|
|
||||||
// animate the drift sequence
|
|
||||||
compat.animate(function render() {
|
|
||||||
if (data.drifting) {
|
|
||||||
var min = 1, max = -1;
|
|
||||||
data.drifting = false;
|
|
||||||
if (yMod > min && target.scrollTop > scrollTop || yMod < max && target.scrollTop < scrollTop) {
|
|
||||||
data.drifting = true;
|
|
||||||
target.scrollTop -= yMod;
|
|
||||||
yMod /= decay;
|
|
||||||
}
|
|
||||||
if (xMod > min && target.scrollLeft > scrollLeft || xMod < max && target.scrollLeft < scrollLeft) {
|
|
||||||
data.drifting = true;
|
|
||||||
target.scrollLeft -= xMod;
|
|
||||||
xMod /= decay;
|
|
||||||
}
|
|
||||||
moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
|
|
||||||
compat.animate(render);
|
|
||||||
} else {
|
|
||||||
triggerEvent('driftend', data.target);
|
|
||||||
callback(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// starts the drag operation and binds the mouse move handler
|
|
||||||
function start(event) {
|
|
||||||
|
|
||||||
var data = event.data,
|
|
||||||
touches = event.originalEvent.touches,
|
|
||||||
target = data.target,
|
|
||||||
dstart = data.start = $(event.target),
|
|
||||||
flags = data.flags;
|
|
||||||
|
|
||||||
// stop any drifts
|
|
||||||
flags.drifting = false;
|
|
||||||
|
|
||||||
// only start drag if the user has not explictly banned it.
|
|
||||||
if (dstart.size() && !dstart.is(data.options.cancelOn)) {
|
|
||||||
|
|
||||||
// without this the simple "click" event won't be recognized on touch clients
|
|
||||||
if (!touches) { event.preventDefault(); }
|
|
||||||
|
|
||||||
if (!compat.overflowScrolling) {
|
|
||||||
target.css('cursor', compat.cursor.grabbing);
|
|
||||||
target.data(datakey).dragging = flags.dragging = flags.dragged = false;
|
|
||||||
|
|
||||||
// apply the drag listeners to the doc or target
|
|
||||||
if(data.options.dragHold) {
|
|
||||||
$(document).on(events.drag, data, drag);
|
|
||||||
} else {
|
|
||||||
target.on(events.drag, data, drag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.position = capturePosition(event, {});
|
|
||||||
data.capture = capturePosition(event, {}, settings.captureThreshold);
|
|
||||||
triggerEvent('dragstart', target);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ends the drag operation and unbinds the mouse move handler
|
|
||||||
function stop(event) {
|
|
||||||
|
|
||||||
var data = event.data,
|
|
||||||
target = data.target,
|
|
||||||
options = data.options,
|
|
||||||
flags = data.flags,
|
|
||||||
thumbs = data.thumbs,
|
|
||||||
|
|
||||||
// hides the thumbs after the animation is done
|
|
||||||
done = function () {
|
|
||||||
if (thumbs && !options.hoverThumbs) {
|
|
||||||
toggleThumbs(thumbs, options, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove drag listeners from doc or target
|
|
||||||
if(options.dragHold) {
|
|
||||||
$(document).unbind(events.drag, drag);
|
|
||||||
} else {
|
|
||||||
target.unbind(events.drag, drag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// only fire events and drift if we started with a
|
|
||||||
// valid position
|
|
||||||
if (data.position) {
|
|
||||||
|
|
||||||
triggerEvent('dragend', target);
|
|
||||||
|
|
||||||
// only drift if a drag passed our threshold
|
|
||||||
if (flags.dragging && !compat.overflowScrolling) {
|
|
||||||
drift(target.get(0), event, done);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// only if we moved, and the mouse down is the same as
|
|
||||||
// the mouse up target do we defer the event
|
|
||||||
if (flags.dragging && !compat.overflowScrolling && data.start && data.start.is(event.target)) {
|
|
||||||
deferClick(data.start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear all internal flags and settings
|
|
||||||
target.data(datakey).dragging =
|
|
||||||
data.start =
|
|
||||||
data.capture =
|
|
||||||
data.position =
|
|
||||||
flags.dragged =
|
|
||||||
flags.dragging = false;
|
|
||||||
|
|
||||||
// set the cursor back to normal
|
|
||||||
target.css('cursor', compat.cursor.grab);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a full set of options are provided
|
|
||||||
// for the plug-in. Also does some validation
|
|
||||||
function getOptions(options) {
|
|
||||||
|
|
||||||
// fill in missing values with defaults
|
|
||||||
options = $.extend({}, defaults, options);
|
|
||||||
|
|
||||||
// check for inconsistent directional restrictions
|
|
||||||
if (options.direction !== 'multi' && options.direction !== options.wheelDirection) {
|
|
||||||
options.wheelDirection = options.direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure positive values for deltas
|
|
||||||
options.scrollDelta = math.abs(parseFloat(options.scrollDelta));
|
|
||||||
options.wheelDelta = math.abs(parseFloat(options.wheelDelta));
|
|
||||||
|
|
||||||
// fix values for scroll offset
|
|
||||||
options.scrollLeft = options.scrollLeft === none ? null : math.abs(parseFloat(options.scrollLeft));
|
|
||||||
options.scrollTop = options.scrollTop === none ? null : math.abs(parseFloat(options.scrollTop));
|
|
||||||
|
|
||||||
return options;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the sizing information (bounding box) for the
|
|
||||||
// target DOM element
|
|
||||||
function getSizing(target) {
|
|
||||||
|
|
||||||
var $target = $(target),
|
|
||||||
width = $target.width(),
|
|
||||||
height = $target.height(),
|
|
||||||
scrollWidth = width >= target.scrollWidth ? width : target.scrollWidth,
|
|
||||||
scrollHeight = height >= target.scrollHeight ? height : target.scrollHeight,
|
|
||||||
hasScroll = scrollWidth > width || scrollHeight > height;
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: hasScroll,
|
|
||||||
container: {
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
scrollWidth: scrollWidth,
|
|
||||||
scrollHeight: scrollHeight
|
|
||||||
},
|
|
||||||
thumbs: {
|
|
||||||
horizontal: {
|
|
||||||
width: width * width / scrollWidth,
|
|
||||||
height: settings.thumbThickness,
|
|
||||||
corner: settings.thumbThickness / 2,
|
|
||||||
left: 0,
|
|
||||||
top: height - settings.thumbThickness
|
|
||||||
},
|
|
||||||
vertical: {
|
|
||||||
width: settings.thumbThickness,
|
|
||||||
height: height * height / scrollHeight,
|
|
||||||
corner: settings.thumbThickness / 2,
|
|
||||||
left: width - settings.thumbThickness,
|
|
||||||
top: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to get (or implicitly creates) the
|
|
||||||
// remover function for the target passed
|
|
||||||
// in as an argument
|
|
||||||
function getRemover(target, orCreate) {
|
|
||||||
|
|
||||||
var $target = $(target), thumbs,
|
|
||||||
data = $target.data(datakey) || {},
|
|
||||||
style = $target.attr('style'),
|
|
||||||
fallback = orCreate ? function () {
|
|
||||||
|
|
||||||
data = $target.data(datakey);
|
|
||||||
thumbs = data.thumbs;
|
|
||||||
|
|
||||||
// restore original styles (if any)
|
|
||||||
if (style) {
|
|
||||||
$target.attr('style', style);
|
|
||||||
} else {
|
|
||||||
$target.removeAttr('style');
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove any created thumbs
|
|
||||||
if (thumbs) {
|
|
||||||
if (thumbs.horizontal) { thumbs.horizontal.remove(); }
|
|
||||||
if (thumbs.vertical) { thumbs.vertical.remove(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove any bound overscroll events and data
|
|
||||||
$target
|
|
||||||
.removeData(datakey)
|
|
||||||
.off(events.wheel, wheel)
|
|
||||||
.off(events.start, start)
|
|
||||||
.off(events.end, stop)
|
|
||||||
.off(events.ignored, ignore);
|
|
||||||
|
|
||||||
} : $.noop;
|
|
||||||
|
|
||||||
return $.isFunction(data.remover) ? data.remover : fallback;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Genterates CSS specific to a particular thumb.
|
|
||||||
// It requires sizing data and options
|
|
||||||
function getThumbCss(size, options) {
|
|
||||||
return {
|
|
||||||
position: 'absolute',
|
|
||||||
opacity: options.persistThumbs ? settings.thumbOpacity : 0,
|
|
||||||
'background-color': 'black',
|
|
||||||
width: size.width + 'px',
|
|
||||||
height: size.height + 'px',
|
|
||||||
'border-radius': size.corner + 'px',
|
|
||||||
'margin': size.top + 'px 0 0 ' + size.left + 'px',
|
|
||||||
'z-index': options.zIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates the DOM elements used as "thumbs" within
|
|
||||||
// the target container.
|
|
||||||
function createThumbs(target, sizing, options) {
|
|
||||||
|
|
||||||
var div = '<div/>',
|
|
||||||
thumbs = {},
|
|
||||||
css = false;
|
|
||||||
|
|
||||||
if (sizing.container.scrollWidth > 0 && options.direction !== 'vertical') {
|
|
||||||
css = getThumbCss(sizing.thumbs.horizontal, options);
|
|
||||||
thumbs.horizontal = $(div).css(css).prependTo(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sizing.container.scrollHeight > 0 && options.direction !== 'horizontal') {
|
|
||||||
css = getThumbCss(sizing.thumbs.vertical, options);
|
|
||||||
thumbs.vertical = $(div).css(css).prependTo(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbs.added = !!css;
|
|
||||||
|
|
||||||
return thumbs;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignores events on the overscroll element
|
|
||||||
function ignore(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function takes a jQuery element, some
|
|
||||||
// (optional) options, and sets up event metadata
|
|
||||||
// for each instance the plug-in affects
|
|
||||||
function setup(target, options) {
|
|
||||||
|
|
||||||
// create initial data properties for this instance
|
|
||||||
options = getOptions(options);
|
|
||||||
var sizing = getSizing(target),
|
|
||||||
thumbs, data = {
|
|
||||||
options: options, sizing: sizing,
|
|
||||||
flags: { dragging: false },
|
|
||||||
remover: getRemover(target, true)
|
|
||||||
};
|
|
||||||
|
|
||||||
// only apply handlers if the overscrolled element
|
|
||||||
// actually has an area to scroll
|
|
||||||
if (sizing.valid || options.ignoreSizing) {
|
|
||||||
// provide a circular-reference, enable events, and
|
|
||||||
// apply any required CSS
|
|
||||||
data.target = target = $(target).css({
|
|
||||||
position: 'relative',
|
|
||||||
cursor: compat.cursor.grab
|
|
||||||
}).on(events.start, data, start)
|
|
||||||
.on(events.end, data, stop)
|
|
||||||
.on(events.ignored, data, ignore);
|
|
||||||
|
|
||||||
// apply the stop listeners for drag end
|
|
||||||
if(options.dragHold) {
|
|
||||||
$(document).on(events.end, data, stop);
|
|
||||||
} else {
|
|
||||||
data.target.on(events.end, data, stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply any user-provided scroll offsets
|
|
||||||
if (options.scrollLeft !== null) {
|
|
||||||
target.scrollLeft(options.scrollLeft);
|
|
||||||
} if (options.scrollTop !== null) {
|
|
||||||
target.scrollTop(options.scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// use native oversroll, if it exists
|
|
||||||
if (compat.overflowScrolling) {
|
|
||||||
target.css(compat.overflowScrolling, 'touch');
|
|
||||||
} else {
|
|
||||||
target.on(events.scroll, data, scroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check to see if the user would like mousewheel support
|
|
||||||
if (options.captureWheel) {
|
|
||||||
target.on(events.wheel, data, wheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add thumbs and listeners (if we're showing them)
|
|
||||||
if (options.showThumbs) {
|
|
||||||
if (compat.overflowScrolling) {
|
|
||||||
target.css('overflow', 'scroll');
|
|
||||||
} else {
|
|
||||||
target.css('overflow', 'hidden');
|
|
||||||
data.thumbs = thumbs = createThumbs(target, sizing, options);
|
|
||||||
if (thumbs.added) {
|
|
||||||
moveThumbs(thumbs, sizing, target.scrollLeft(), target.scrollTop());
|
|
||||||
if (options.hoverThumbs) {
|
|
||||||
target.on(events.hover, data, hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
target.css('overflow', 'hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
target.data(datakey, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes any event listeners and other instance-specific
|
|
||||||
// data from the target. It attempts to leave the target
|
|
||||||
// at the state it found it.
|
|
||||||
function teardown(target) {
|
|
||||||
getRemover(target)();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the entry-point for enabling the plug-in;
|
|
||||||
// You can find it's exposure point at the end
|
|
||||||
// of this closure
|
|
||||||
function overscroll(options) {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
return this.removeOverscroll().each(function() {
|
|
||||||
setup(this, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the entry-point for disabling the plug-in;
|
|
||||||
// You can find it's exposure point at the end
|
|
||||||
// of this closure
|
|
||||||
function removeOverscroll() {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
return this.each(function () {
|
|
||||||
teardown(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend overscroll to expose settings to the user
|
|
||||||
overscroll.settings = settings;
|
|
||||||
|
|
||||||
// Extend jQuery's prototype to expose the plug-in.
|
|
||||||
// If the supports native overflowScrolling, overscroll will not
|
|
||||||
// attempt to override the browser's built in support
|
|
||||||
$.extend(namespace, {
|
|
||||||
overscroll: overscroll,
|
|
||||||
removeOverscroll: removeOverscroll
|
|
||||||
});
|
|
||||||
|
|
||||||
})(window, document, navigator, Math, setTimeout, clearTimeout, jQuery.fn, jQuery);
|
|
14346
static/lib/nv.d3.js
14346
static/lib/nv.d3.js
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,8 @@ if (typeof exports === "object" && typeof require === "function") // we're in a
|
||||||
Markdown = exports;
|
Markdown = exports;
|
||||||
else
|
else
|
||||||
Markdown = {};
|
Markdown = {};
|
||||||
|
|
||||||
|
window.Markdown = Markdown;
|
||||||
|
|
||||||
// The following text is included for historical reasons, but should
|
// The following text is included for historical reasons, but should
|
||||||
// be taken with a pinch of salt; it's not all true anymore.
|
// be taken with a pinch of salt; it's not all true anymore.
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
||||||
<span data-title="{{change.file}}">
|
<span data-title="{{change.file}}">
|
||||||
<span style="color: #888;">
|
<span style="color: #888;">
|
||||||
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
<span ng-repeat="folder in getFolders(change.file) track by $index"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
58
static/partials/landing-login.html
Normal file
58
static/partials/landing-login.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<div class="landing-content">
|
||||||
|
<div class="jumbotron landing">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row messages">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div ng-show="user.anonymous" style="text-align: center">
|
||||||
|
<span style="display: inline-block; background: white; padding: 10px; border-radius: 10px;">
|
||||||
|
<img ng-src="{{ getEnterpriseLogo() }}" style="max-height: 100px;">
|
||||||
|
</span>
|
||||||
|
<h1>Quay.io Enterprise Edition</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!user.anonymous">
|
||||||
|
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
|
||||||
|
|
||||||
|
<div class="resource-view" resource="my_repositories">
|
||||||
|
<!-- Repos -->
|
||||||
|
<div ng-show="my_repositories.value.length > 0">
|
||||||
|
<h2>Top Repositories</h2>
|
||||||
|
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
|
||||||
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
|
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Repos -->
|
||||||
|
<div ng-show="my_repositories.value.length == 0">
|
||||||
|
<div class="sub-message" style="margin-top: 20px">
|
||||||
|
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
|
||||||
|
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
|
||||||
|
<div class="options">
|
||||||
|
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- col -->
|
||||||
|
|
||||||
|
<div class="col-md-4 col-md-offset-1">
|
||||||
|
<div ng-show="user.anonymous">
|
||||||
|
<h3>Create Username</h3>
|
||||||
|
<div class="signup-form"></div>
|
||||||
|
</div>
|
||||||
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
|
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||||
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
|
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
|
</div>
|
||||||
|
</div> <!-- col -->
|
||||||
|
</div> <!-- row -->
|
||||||
|
|
||||||
|
</div> <!-- container -->
|
||||||
|
</div> <!-- jumbotron -->
|
||||||
|
</div>
|
152
static/partials/landing-normal.html
Normal file
152
static/partials/landing-normal.html
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<div class="landing-content">
|
||||||
|
<div class="jumbotron landing">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row messages">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div ng-show="user.anonymous">
|
||||||
|
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
|
||||||
|
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
||||||
|
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!user.anonymous">
|
||||||
|
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
|
||||||
|
|
||||||
|
<div class="resource-view" resource="my_repositories">
|
||||||
|
<!-- Repos -->
|
||||||
|
<div ng-show="my_repositories.value.length > 0">
|
||||||
|
<h2>Top Repositories</h2>
|
||||||
|
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
|
||||||
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
|
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Repos -->
|
||||||
|
<div ng-show="my_repositories.value.length == 0">
|
||||||
|
<div class="sub-message" style="margin-top: 20px">
|
||||||
|
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
|
||||||
|
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
|
||||||
|
<div class="options">
|
||||||
|
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- col -->
|
||||||
|
|
||||||
|
<div class="col-md-4 col-md-offset-1">
|
||||||
|
<div ng-show="user.anonymous">
|
||||||
|
<div class="signup-form"></div>
|
||||||
|
</div>
|
||||||
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
|
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||||
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
|
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
|
</div>
|
||||||
|
</div> <!-- col -->
|
||||||
|
</div> <!-- row -->
|
||||||
|
|
||||||
|
<div class="row" ng-show="user.anonymous">
|
||||||
|
<div class="col-md-4 shoutout">
|
||||||
|
<i class="fa fa-lock"></i>
|
||||||
|
<b>Secure</b>
|
||||||
|
<span class="shoutout-expand">
|
||||||
|
Your data is transferred using <strong>SSL at all times</strong> and <strong>encrypted</strong> when at rest. More information available in our <a href="/security/">security guide</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 shoutout">
|
||||||
|
<i class="fa fa-group"></i>
|
||||||
|
<b>Shareable</b>
|
||||||
|
<span class="shoutout-expand">
|
||||||
|
Have to share a repository? No problem! Share with anyone you choose
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 shoutout">
|
||||||
|
<i class="fa fa-cloud"></i>
|
||||||
|
<b>Cloud Hosted</b>
|
||||||
|
<span class="shoutout-expand">
|
||||||
|
Accessible from anywhere, anytime
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div> <!-- row -->
|
||||||
|
</div> <!-- container -->
|
||||||
|
</div> <!-- jumbotron -->
|
||||||
|
|
||||||
|
<div class="product-tour container" ng-show="user.anonymous">
|
||||||
|
<div class="tour-header row">
|
||||||
|
<div class="tour-shoutout-header"><i class="fa fa-chevron-circle-down"></i></div>
|
||||||
|
<div class="tour-shoutout">Take a tour of Quay</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7"><img src="/static/img/user-home.png" title="User Home - Quay.io" data-screenshot-url="https://quay.io/" class="img-responsive"></div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="tour-section-title">Customized for you</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
Your personal home screen shows those repositories most important to you, ordered by recent activity.
|
||||||
|
</div>
|
||||||
|
<div class="tour-section-description">Keep up to date on the status of those repositories you deem important.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-view.png" title="Repository View - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex" class="img-responsive"></div>
|
||||||
|
<div class="col-md-5 col-md-pull-7">
|
||||||
|
<div class="tour-section-title">Useful views of respositories</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
Each repository is presented with the maximum amount of useful information, including its image history, <b>markdown</b>-based description, and tags.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7"><img src="/static/img/build-history.png" title="View Image - Quay.io"
|
||||||
|
data-screenshot-url="https://quay.io/repository/devtable/building/build"
|
||||||
|
class="img-responsive"></div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="tour-section-title">Dockerfile Build in the cloud</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
Like to use <b>Dockerfiles</b> to build your images? Simply upload your Dockerfile (and any additional files it needs) and we'll build your Dockerfile into an image and push it to your repository.
|
||||||
|
</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
If you store your Dockerfile in <i class="fa fa-github fa-lg" style="margin: 6px;"></i><b>GitHub</b>, add a <b>Build Trigger</b> to your repository and we'll start a Dockerfile build for every change you make.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-admin.png" title="Repository Admin - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex/admin" class="img-responsive"></div>
|
||||||
|
<div class="col-md-5 col-md-pull-7">
|
||||||
|
<div class="tour-section-title">Share at your control</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
Share any repository with as many (or as few) users as you choose.
|
||||||
|
</div>
|
||||||
|
<div class="tour-section-description">Need a repository only for your team? Easily <b>share</b> with your team members.</div>
|
||||||
|
<div class="tour-section-description">Need finer grain control? Mark a user as <b>read-only</b> or <b>read/write</b>.</div>
|
||||||
|
<div class="tour-section-description">Have a build script or a deploy process that needs access? Generate an <b>access token</b> to grant revocable access for pushing or pulling.</div>
|
||||||
|
<div class="tour-section-description">Want to share with the world? Make your repository <b>fully public</b>.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7"><img src="/static/img/repo-changes.png" title="View Image - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="tour-section-title">Docker diff whenever you need it</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were <b>added</b>, <b>changed</b>, or <b>removed</b> for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-top: 1px solid #eee; padding-top: 20px;">
|
||||||
|
<a href="https://mixpanel.com/f/partner"><img src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png" alt="Mobile Analytics" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue