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
|
||||
static/snapshots/
|
||||
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:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core
|
||||
```
|
||||
|
||||
check out the code:
|
||||
|
||||
```
|
||||
git clone https://bitbucket.org/yackob03/quay.git
|
||||
cd quay
|
||||
virtualenv --distribute venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
sudo gdebi --n binary_dependencies/*.deb
|
||||
sudo cp conf/logrotate/* /etc/logrotate.d/
|
||||
```
|
||||
|
||||
running:
|
||||
|
||||
```
|
||||
sudo mkdir -p /mnt/logs/ && sudo chown $USER /mnt/logs/ && sudo /usr/local/nginx/sbin/nginx -c `pwd`/conf/nginx.conf
|
||||
sudo mkdir -p /mnt/logs/ && sudo chown $USER /mnt/logs/ && STACK=prod gunicorn -c conf/gunicorn_config.py application:application
|
||||
```
|
||||
|
||||
start the log shipper:
|
||||
to build and upload quay to quay:
|
||||
|
||||
```
|
||||
curl -s https://get.docker.io/ubuntu/ | sudo sh
|
||||
sudo apt-get update && sudo apt-get install -y git
|
||||
git clone git clone https://bitbucket.org/yackob03/quay.git
|
||||
cd quay
|
||||
sudo docker build -t quay.io/quay/quay .
|
||||
sudo docker push quay.io/quay/quay
|
||||
```
|
||||
|
||||
to prepare a new host:
|
||||
|
||||
```
|
||||
curl -s https://get.docker.io/ubuntu/ | sudo sh
|
||||
sudo apt-get update && sudo apt-get install -y git
|
||||
git clone https://github.com/DevTable/gantryd.git
|
||||
cd gantryd
|
||||
cat requirements.system | xargs sudo apt-get install -y
|
||||
virtualenv --distribute venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
sudo docker login -p 9Y1PX7D3IE4KPSGCIALH17EM5V3ZTMP8CNNHJNXAQ2NJGAS48BDH8J1PUOZ869ML -u 'quay+deploy' -e notused quay.io
|
||||
```
|
||||
|
||||
start the quay processes:
|
||||
|
||||
```
|
||||
cd ~
|
||||
git clone https://bitbucket.org/yackob03/quayconfig.git
|
||||
sudo docker pull quay.io/quay/quay
|
||||
sudo mkdir -p /mnt/logs/
|
||||
cd ~/gantryd
|
||||
sudo venv/bin/python gantry.py ../quayconfig/production/gantry.json update quay
|
||||
```
|
||||
|
||||
start the log shipper (DEPRECATED):
|
||||
|
||||
```
|
||||
sudo docker pull quay.io/quay/logstash
|
||||
sudo docker run -d -e REDIS_PORT_6379_TCP_ADDR=logs.quay.io -v /mnt/logs:/mnt/logs quay.io/quay/logstash quay.conf
|
||||
```
|
||||
|
||||
start the workers:
|
||||
|
||||
```
|
||||
STACK=prod python -m workers.diffsworker -D
|
||||
STACK=prod python -m workers.webhookworker -D
|
||||
```
|
||||
|
||||
bouncing the servers:
|
||||
|
||||
```
|
||||
sudo kill -HUP `cat /mnt/logs/nginx.pid`
|
||||
kill -HUP `cat /mnt/logs/gunicorn.pid`
|
||||
|
||||
kill <pids of worker daemons>
|
||||
restart daemons
|
||||
```
|
||||
|
||||
running the tests:
|
||||
|
||||
```
|
||||
STACK=test python -m unittest discover
|
||||
TEST=true python -m unittest discover
|
||||
```
|
||||
|
||||
running the tests with coverage (requires coverage module):
|
||||
|
||||
```
|
||||
STACK=test coverage run -m unittest discover
|
||||
TEST=true coverage run -m unittest discover
|
||||
coverage html
|
||||
```
|
||||
|
||||
|
|
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 os
|
||||
import stripe
|
||||
|
||||
from flask import Flask
|
||||
from flask.ext.principal import Principal
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.mail import Mail
|
||||
|
||||
from config import (ProductionConfig, DebugConfig, LocalHostedConfig,
|
||||
TestConfig, StagingConfig)
|
||||
from util import analytics
|
||||
import features
|
||||
|
||||
from storage import Storage
|
||||
from data.userfiles import Userfiles
|
||||
from util.analytics import Analytics
|
||||
from util.exceptionlog import Sentry
|
||||
from data.billing import Billing
|
||||
|
||||
|
||||
OVERRIDE_CONFIG_FILENAME = 'conf/stack/config.py'
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
stack = os.environ.get('STACK', '').strip().lower()
|
||||
if stack.startswith('prod'):
|
||||
logger.info('Running with production config.')
|
||||
config = ProductionConfig()
|
||||
elif stack.startswith('staging'):
|
||||
logger.info('Running with staging config on production data.')
|
||||
config = StagingConfig()
|
||||
elif stack.startswith('localhosted'):
|
||||
logger.info('Running with debug config on production data.')
|
||||
config = LocalHostedConfig()
|
||||
elif stack.startswith('test'):
|
||||
logger.info('Running with test config on ephemeral data.')
|
||||
config = TestConfig()
|
||||
if 'TEST' in os.environ:
|
||||
from test.testconfig import TestConfig
|
||||
logger.debug('Loading test config.')
|
||||
app.config.from_object(TestConfig())
|
||||
else:
|
||||
logger.info('Running with debug config.')
|
||||
config = DebugConfig()
|
||||
from config import DefaultConfig
|
||||
logger.debug('Loading default config.')
|
||||
app.config.from_object(DefaultConfig())
|
||||
|
||||
app.config.from_object(config)
|
||||
if os.path.exists(OVERRIDE_CONFIG_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME)
|
||||
app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME)
|
||||
|
||||
features.import_features(app.config)
|
||||
|
||||
Principal(app, use_sessions=False)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
|
||||
mail = Mail()
|
||||
mail.init_app(app)
|
||||
|
||||
stripe.api_key = app.config.get('STRIPE_SECRET_KEY', None)
|
||||
|
||||
mixpanel = app.config['ANALYTICS'].init_app(app)
|
||||
login_manager = LoginManager(app)
|
||||
mail = Mail(app)
|
||||
storage = Storage(app)
|
||||
userfiles = Userfiles(app)
|
||||
analytics = Analytics(app)
|
||||
billing = Billing(app)
|
||||
sentry = Sentry(app)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import logging
|
||||
import logging.config
|
||||
import uuid
|
||||
|
||||
from app import app as application
|
||||
from data.model import db as model_db
|
||||
from flask import request, Request
|
||||
from util.names import urn_generator
|
||||
|
||||
# Initialize logging
|
||||
application.config['LOGGING_CONFIG']()
|
||||
from data.model import db as model_db
|
||||
|
||||
# Turn off debug logging for boto
|
||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
|
@ -20,6 +22,7 @@ from endpoints.callbacks import callback
|
|||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
profile = logging.getLogger('application.profiler')
|
||||
|
||||
application.register_blueprint(web)
|
||||
application.register_blueprint(callback, url_prefix='/oauth2')
|
||||
|
@ -30,6 +33,29 @@ application.register_blueprint(api_bp, url_prefix='/api')
|
|||
application.register_blueprint(webhooks, url_prefix='/webhooks')
|
||||
application.register_blueprint(realtime, url_prefix='/realtime')
|
||||
|
||||
class RequestWithId(Request):
|
||||
request_gen = staticmethod(urn_generator(['request']))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RequestWithId, self).__init__(*args, **kwargs)
|
||||
self.request_id = self.request_gen()
|
||||
|
||||
@application.before_request
|
||||
def _request_start():
|
||||
profile.debug('Starting request: %s', request.path)
|
||||
|
||||
|
||||
@application.after_request
|
||||
def _request_end(r):
|
||||
profile.debug('Ending request: %s', request.path)
|
||||
return r
|
||||
|
||||
class InjectingFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.msg = '[%s] %s' % (request.request_id, record.msg)
|
||||
return True
|
||||
|
||||
profile.addFilter(InjectingFilter())
|
||||
|
||||
def close_db(exc):
|
||||
db = model_db
|
||||
|
@ -38,6 +64,8 @@ def close_db(exc):
|
|||
db.close()
|
||||
|
||||
application.teardown_request(close_db)
|
||||
application.request_class = RequestWithId
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.config.fileConfig('conf/logging_local.conf', disable_existing_loggers=False)
|
||||
application.run(port=5000, debug=True, threaded=True, host='0.0.0.0')
|
||||
|
|
|
@ -22,6 +22,7 @@ _TeamTypeNeed = namedtuple('teamwideneed', ['type', 'orgname', 'teamname', 'role
|
|||
_TeamNeed = partial(_TeamTypeNeed, 'orgteam')
|
||||
_UserTypeNeed = namedtuple('userspecificneed', ['type', 'username', 'role'])
|
||||
_UserNeed = partial(_UserTypeNeed, 'user')
|
||||
_SuperUserNeed = partial(namedtuple('superuserneed', ['type']), 'superuser')
|
||||
|
||||
|
||||
REPO_ROLES = [None, 'read', 'write', 'admin']
|
||||
|
@ -88,6 +89,11 @@ class QuayDeferredPermissionUser(Identity):
|
|||
logger.debug('Loading user permissions after deferring.')
|
||||
user_object = model.get_user(self.id)
|
||||
|
||||
# Add the superuser need, if applicable.
|
||||
if (user_object.username is not None and
|
||||
user_object.username in app.config.get('SUPER_USERS', [])):
|
||||
self.provides.add(_SuperUserNeed())
|
||||
|
||||
# Add the user specific permissions, only for non-oauth permission
|
||||
user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin'))
|
||||
logger.debug('User permission: {0}'.format(user_grant))
|
||||
|
@ -171,6 +177,11 @@ class CreateRepositoryPermission(Permission):
|
|||
super(CreateRepositoryPermission, self).__init__(admin_org,
|
||||
create_repo_org)
|
||||
|
||||
class SuperUserPermission(Permission):
|
||||
def __init__(self):
|
||||
need = _SuperUserNeed()
|
||||
super(SuperUserPermission, self).__init__(need)
|
||||
|
||||
|
||||
class UserAdminPermission(Permission):
|
||||
def __init__(self, username):
|
||||
|
|
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
|
||||
worker_class = 'gevent'
|
||||
timeout = 2000
|
||||
daemon = True
|
||||
pidfile = '/mnt/logs/gunicorn.pid'
|
||||
errorlog = '/mnt/logs/application.log'
|
||||
loglevel = 'debug'
|
||||
logger_class = 'util.glogger.LogstashLogger'
|
||||
pidfile = '/tmp/gunicorn.pid'
|
||||
logconfig = 'conf/logging.conf'
|
||||
pythonpath = '.'
|
|
@ -3,7 +3,5 @@ workers = 2
|
|||
worker_class = 'gevent'
|
||||
timeout = 2000
|
||||
daemon = False
|
||||
errorlog = '-'
|
||||
loglevel = 'debug'
|
||||
logger_class = 'util.glogger.LogstashLogger'
|
||||
logconfig = 'conf/logging_local.conf'
|
||||
pythonpath = '.'
|
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;
|
||||
|
||||
user root nogroup;
|
||||
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
server {
|
||||
include server-base.conf;
|
||||
|
||||
listen 5000 default;
|
||||
listen 80 default;
|
||||
|
||||
location /static/ {
|
||||
# checks for static file, if not found proxy to app
|
||||
alias /home/jake/Projects/docker/quay/static/;
|
||||
alias /static/;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ worker_processes 2;
|
|||
|
||||
user root nogroup;
|
||||
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
|
@ -15,8 +17,8 @@ http {
|
|||
listen 443 default;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate ./certs/quay-staging-unified.cert;
|
||||
ssl_certificate_key ./certs/quay-staging.key;
|
||||
ssl_certificate ./stack/ssl.cert;
|
||||
ssl_certificate_key ./stack/ssl.key;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_protocols SSLv3 TLSv1;
|
||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
||||
|
@ -24,7 +26,7 @@ http {
|
|||
|
||||
location /static/ {
|
||||
# checks for static file, if not found proxy to app
|
||||
alias /root/quay/static/;
|
||||
alias /static/;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
events {
|
||||
|
|
350
config.py
350
config.py
|
@ -1,198 +1,8 @@
|
|||
import logging
|
||||
import logstash_formatter
|
||||
import requests
|
||||
import os.path
|
||||
|
||||
from peewee import MySQLDatabase, SqliteDatabase
|
||||
from storage.s3 import S3Storage
|
||||
from storage.local import LocalStorage
|
||||
from data.userfiles import UserRequestFiles
|
||||
from data.buildlogs import BuildLogs
|
||||
from data.userevent import UserEventBuilder
|
||||
from util import analytics
|
||||
|
||||
from test.teststorage import FakeStorage, FakeUserfiles
|
||||
from test import analytics as fake_analytics
|
||||
from test.testlogs import TestBuildLogs
|
||||
|
||||
|
||||
class FlaskConfig(object):
|
||||
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
||||
JSONIFY_PRETTYPRINT_REGULAR = False
|
||||
|
||||
|
||||
class FlaskProdConfig(FlaskConfig):
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
|
||||
class MailConfig(object):
|
||||
MAIL_SERVER = 'email-smtp.us-east-1.amazonaws.com'
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_PORT = 587
|
||||
MAIL_USERNAME = 'AKIAIXV5SDGCPVMU3N4Q'
|
||||
MAIL_PASSWORD = 'AhmX/vWE91uQ2RtcEKTkfNrzZehEjPNXOXeOXgQNfLao'
|
||||
DEFAULT_MAIL_SENDER = 'support@quay.io'
|
||||
MAIL_FAIL_SILENTLY = False
|
||||
TESTING = False
|
||||
|
||||
|
||||
class RealTransactions(object):
|
||||
@staticmethod
|
||||
def create_transaction(db):
|
||||
return db.transaction()
|
||||
|
||||
DB_TRANSACTION_FACTORY = create_transaction
|
||||
|
||||
|
||||
class SQLiteDB(RealTransactions):
|
||||
DB_NAME = 'test/data/test.db'
|
||||
DB_CONNECTION_ARGS = {
|
||||
'threadlocals': True,
|
||||
'autorollback': True,
|
||||
}
|
||||
DB_DRIVER = SqliteDatabase
|
||||
|
||||
|
||||
class FakeTransaction(object):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, value, traceback):
|
||||
pass
|
||||
|
||||
|
||||
class EphemeralDB(object):
|
||||
DB_NAME = ':memory:'
|
||||
DB_CONNECTION_ARGS = {}
|
||||
DB_DRIVER = SqliteDatabase
|
||||
|
||||
@staticmethod
|
||||
def create_transaction(db):
|
||||
return FakeTransaction()
|
||||
|
||||
DB_TRANSACTION_FACTORY = create_transaction
|
||||
|
||||
|
||||
class RDSMySQL(RealTransactions):
|
||||
DB_NAME = 'quay'
|
||||
DB_CONNECTION_ARGS = {
|
||||
'host': 'fluxmonkeylogin.cb0vumcygprn.us-east-1.rds.amazonaws.com',
|
||||
'user': 'fluxmonkey',
|
||||
'passwd': '8eifM#uoZ85xqC^',
|
||||
'threadlocals': True,
|
||||
'autorollback': True,
|
||||
}
|
||||
DB_DRIVER = MySQLDatabase
|
||||
|
||||
|
||||
class AWSCredentials(object):
|
||||
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
||||
AWS_SECRET_KEY = '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():
|
||||
|
@ -204,70 +14,124 @@ def build_requests_session():
|
|||
return sess
|
||||
|
||||
|
||||
class LargePoolHttpClient(object):
|
||||
# The set of configuration key names that will be accessible in the client. Since these
|
||||
# values are set to the frontend, DO NOT PLACE ANY SECRETS OR KEYS in this list.
|
||||
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
|
||||
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
|
||||
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN']
|
||||
|
||||
|
||||
def getFrontendVisibleConfig(config_dict):
|
||||
visible_dict = {}
|
||||
for name in CLIENT_WHITELIST:
|
||||
if name.lower().find('secret') >= 0:
|
||||
raise Exception('Cannot whitelist secrets: %s' % name)
|
||||
|
||||
if name in config_dict:
|
||||
visible_dict[name] = config_dict.get(name, None)
|
||||
|
||||
return visible_dict
|
||||
|
||||
|
||||
class DefaultConfig(object):
|
||||
# Flask config
|
||||
SECRET_KEY = 'a36c9d7d-25a9-4d3f-a586-3d2f8dc40a83'
|
||||
JSONIFY_PRETTYPRINT_REGULAR = False
|
||||
SESSION_COOKIE_SECURE = False
|
||||
|
||||
LOGGING_LEVEL = 'DEBUG'
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
PREFERRED_URL_SCHEME = 'http'
|
||||
SERVER_HOSTNAME = 'localhost:5000'
|
||||
|
||||
# Mail config
|
||||
MAIL_SERVER = ''
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_PORT = 587
|
||||
MAIL_USERNAME = ''
|
||||
MAIL_PASSWORD = ''
|
||||
DEFAULT_MAIL_SENDER = ''
|
||||
MAIL_FAIL_SILENTLY = False
|
||||
TESTING = True
|
||||
|
||||
# DB config
|
||||
DB_URI = 'sqlite:///test/data/test.db'
|
||||
DB_CONNECTION_ARGS = {
|
||||
'threadlocals': True,
|
||||
'autorollback': True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_transaction(db):
|
||||
return db.transaction()
|
||||
|
||||
DB_TRANSACTION_FACTORY = create_transaction
|
||||
|
||||
# Data storage
|
||||
STORAGE_TYPE = 'LocalStorage'
|
||||
STORAGE_PATH = 'test/data/registry'
|
||||
|
||||
# Build logs
|
||||
BUILDLOGS = BuildLogs('logs.quay.io') # Change me
|
||||
|
||||
# Real-time user events
|
||||
USER_EVENTS = UserEventBuilder('logs.quay.io')
|
||||
|
||||
# Stripe config
|
||||
BILLING_TYPE = 'FakeStripe'
|
||||
|
||||
# Userfiles
|
||||
USERFILES_TYPE = 'LocalUserfiles'
|
||||
USERFILES_PATH = 'test/data/registry/userfiles'
|
||||
|
||||
# Analytics
|
||||
ANALYTICS_TYPE = "FakeAnalytics"
|
||||
|
||||
# Exception logging
|
||||
EXCEPTION_LOG_TYPE = 'FakeSentry'
|
||||
SENTRY_DSN = None
|
||||
SENTRY_PUBLIC_DSN = None
|
||||
|
||||
# Github Config
|
||||
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||
GITHUB_USER_URL = 'https://api.github.com/user'
|
||||
GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails'
|
||||
|
||||
GITHUB_CLIENT_ID = ''
|
||||
GITHUB_CLIENT_SECRET = ''
|
||||
|
||||
GITHUB_LOGIN_CLIENT_ID = ''
|
||||
GITHUB_LOGIN_CLIENT_SECRET = ''
|
||||
|
||||
# Requests based HTTP client with a large request pool
|
||||
HTTPCLIENT = build_requests_session()
|
||||
|
||||
|
||||
class StatusTagConfig(object):
|
||||
# Status tag config
|
||||
STATUS_TAGS = {}
|
||||
|
||||
for tag_name in ['building', 'failed', 'none', 'ready']:
|
||||
tag_path = os.path.join('buildstatus', tag_name + '.svg')
|
||||
with open(tag_path) as tag_svg:
|
||||
STATUS_TAGS[tag_name] = tag_svg.read()
|
||||
|
||||
WEBHOOK_QUEUE_NAME = 'webhook'
|
||||
DIFFS_QUEUE_NAME = 'imagediff'
|
||||
DOCKERFILE_BUILD_QUEUE_NAME = 'dockerfilebuild'
|
||||
|
||||
class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
|
||||
FakeAnalytics, StripeTestConfig, RedisBuildLogs,
|
||||
UserEventConfig, LargePoolHttpClient, StatusTagConfig):
|
||||
LOGGING_CONFIG = logs_init_builder(logging.WARN)
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
TESTING = True
|
||||
URL_SCHEME = 'http'
|
||||
URL_HOST = 'localhost:5000'
|
||||
# Super user config. Note: This MUST BE an empty list for the default config.
|
||||
SUPER_USERS = []
|
||||
|
||||
# Feature Flag: Whether billing is required.
|
||||
FEATURE_BILLING = True
|
||||
|
||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
||||
DigitalOceanConfig, BuildNodeConfig, S3Userfiles,
|
||||
UserEventConfig, TestBuildLogs, LargePoolHttpClient,
|
||||
StatusTagConfig):
|
||||
LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter())
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
URL_SCHEME = 'http'
|
||||
URL_HOST = 'ci.devtable.com:5000'
|
||||
# Feature Flag: Whether user accounts automatically have usage log access.
|
||||
FEATURE_USER_LOG_ACCESS = False
|
||||
|
||||
# Feature Flag: Whether GitHub login is supported.
|
||||
FEATURE_GITHUB_LOGIN = False
|
||||
|
||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelTestConfig,
|
||||
GitHubProdConfig, DigitalOceanConfig,
|
||||
BuildNodeConfig, S3Userfiles, RedisBuildLogs,
|
||||
UserEventConfig, LargePoolHttpClient,
|
||||
StatusTagConfig):
|
||||
LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter())
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
URL_SCHEME = 'http'
|
||||
URL_HOST = 'ci.devtable.com:5000'
|
||||
# Feature flag, whether to enable olark chat
|
||||
FEATURE_OLARK_CHAT = False
|
||||
|
||||
|
||||
class StagingConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelProdConfig,
|
||||
GitHubStagingConfig, DigitalOceanConfig, BuildNodeConfig,
|
||||
S3Userfiles, RedisBuildLogs, UserEventConfig,
|
||||
LargePoolHttpClient, StatusTagConfig):
|
||||
LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter())
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
URL_SCHEME = 'https'
|
||||
URL_HOST = 'staging.quay.io'
|
||||
|
||||
|
||||
class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelProdConfig,
|
||||
GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig,
|
||||
S3Userfiles, RedisBuildLogs, UserEventConfig,
|
||||
LargePoolHttpClient, StatusTagConfig):
|
||||
LOGGING_CONFIG = logs_init_builder()
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
URL_SCHEME = 'https'
|
||||
URL_HOST = 'quay.io'
|
||||
# Feature Flag: Whether super users are supported.
|
||||
FEATURE_SUPER_USERS = False
|
||||
|
|
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 datetime import datetime
|
||||
from peewee import *
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from urlparse import urlparse
|
||||
|
||||
from app import app
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
db = app.config['DB_DRIVER'](app.config['DB_NAME'],
|
||||
**app.config['DB_CONNECTION_ARGS'])
|
||||
|
||||
|
||||
SCHEME_DRIVERS = {
|
||||
'mysql': MySQLDatabase,
|
||||
'sqlite': SqliteDatabase,
|
||||
}
|
||||
|
||||
|
||||
def generate_db(config_object):
|
||||
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
|
||||
parsed_url = make_url(config_object['DB_URI'])
|
||||
|
||||
if parsed_url.host:
|
||||
db_kwargs['host'] = parsed_url.host
|
||||
if parsed_url.port:
|
||||
db_kwargs['port'] = parsed_url.port
|
||||
if parsed_url.username:
|
||||
db_kwargs['user'] = parsed_url.username
|
||||
if parsed_url.password:
|
||||
db_kwargs['passwd'] = parsed_url.password
|
||||
|
||||
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
||||
|
||||
|
||||
db = generate_db(app.config)
|
||||
|
||||
|
||||
def random_string_generator(length=16):
|
||||
def random_string():
|
||||
|
@ -194,7 +220,8 @@ class ImageStorage(BaseModel):
|
|||
created = DateTimeField(null=True)
|
||||
comment = TextField(null=True)
|
||||
command = TextField(null=True)
|
||||
image_size = BigIntegerField(null=True)
|
||||
image_size = BigIntegerField(null=True)
|
||||
uploading = BooleanField(default=True, null=True)
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
|
@ -249,7 +276,7 @@ class RepositoryBuild(BaseModel):
|
|||
|
||||
|
||||
class QueueItem(BaseModel):
|
||||
queue_name = CharField(index=True)
|
||||
queue_name = CharField(index=True, max_length=1024)
|
||||
body = TextField()
|
||||
available_after = DateTimeField(default=datetime.now, index=True)
|
||||
available = BooleanField(default=True, index=True)
|
||||
|
|
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 json
|
||||
|
||||
|
||||
from data.database import *
|
||||
from util.validation import *
|
||||
from util.names import format_robot_username
|
||||
|
||||
from app import storage as store
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
store = app.config['STORAGE']
|
||||
transaction_factory = app.config['DB_TRANSACTION_FACTORY']
|
||||
|
||||
class DataModelException(Exception):
|
||||
|
@ -817,7 +817,7 @@ def get_repository(namespace_name, repository_name):
|
|||
|
||||
def get_repo_image(namespace_name, repository_name, image_id):
|
||||
query = (Image
|
||||
.select()
|
||||
.select(Image, ImageStorage)
|
||||
.join(Repository)
|
||||
.switch(Image)
|
||||
.join(ImageStorage, JOIN_LEFT_OUTER)
|
||||
|
@ -1489,7 +1489,8 @@ def get_pull_credentials(robotname):
|
|||
return {
|
||||
'username': robot.username,
|
||||
'password': login_info.service_ident,
|
||||
'registry': '%s://%s/v1/' % (app.config['URL_SCHEME'], app.config['URL_HOST']),
|
||||
'registry': '%s://%s/v1/' % (app.config['PREFERRED_URL_SCHEME'],
|
||||
app.config['SERVER_HOSTNAME']),
|
||||
}
|
||||
|
||||
|
||||
|
@ -1521,8 +1522,7 @@ def delete_webhook(namespace_name, repository_name, public_id):
|
|||
return webhook
|
||||
|
||||
|
||||
def list_logs(user_or_organization_name, start_time, end_time, performer=None,
|
||||
repository=None):
|
||||
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
||||
joined = LogEntry.select().join(User)
|
||||
if repository:
|
||||
joined = joined.where(LogEntry.repository == repository)
|
||||
|
@ -1530,8 +1530,10 @@ def list_logs(user_or_organization_name, start_time, end_time, performer=None,
|
|||
if performer:
|
||||
joined = joined.where(LogEntry.performer == performer)
|
||||
|
||||
if namespace:
|
||||
joined = joined.where(User.username == namespace)
|
||||
|
||||
return joined.where(
|
||||
User.username == user_or_organization_name,
|
||||
LogEntry.datetime >= start_time,
|
||||
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc())
|
||||
|
||||
|
@ -1633,3 +1635,15 @@ def delete_notifications_by_kind(target, kind):
|
|||
kind_ref = NotificationKind.get(name=kind)
|
||||
Notification.delete().where(Notification.target == target,
|
||||
Notification.kind == kind_ref).execute()
|
||||
|
||||
|
||||
def get_active_users():
|
||||
return User.select().where(User.organization == False, User.robot == False)
|
||||
|
||||
def get_active_user_count():
|
||||
return get_active_users().count()
|
||||
|
||||
def delete_user(user):
|
||||
user.delete_instance(recursive=True, delete_nullable=True)
|
||||
|
||||
# TODO: also delete any repository data associated
|
||||
|
|
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):
|
||||
def __init__(self, queue_name):
|
||||
def __init__(self, queue_name, canonical_name_match_list=None):
|
||||
self.queue_name = queue_name
|
||||
|
||||
def put(self, message, available_after=0, retries_remaining=5):
|
||||
if canonical_name_match_list is None:
|
||||
self.canonical_name_match_list = []
|
||||
else:
|
||||
self.canonical_name_match_list = canonical_name_match_list
|
||||
|
||||
@staticmethod
|
||||
def _canonical_name(name_list):
|
||||
return '/'.join(name_list) + '/'
|
||||
|
||||
def put(self, canonical_name_list, message, available_after=0, retries_remaining=5):
|
||||
"""
|
||||
Put an item, if it shouldn't be processed for some number of seconds,
|
||||
specify that amount as available_after.
|
||||
"""
|
||||
|
||||
params = {
|
||||
'queue_name': self.queue_name,
|
||||
'queue_name': self._canonical_name([self.queue_name] + canonical_name_list),
|
||||
'body': message,
|
||||
'retries_remaining': retries_remaining,
|
||||
}
|
||||
|
@ -35,16 +44,25 @@ class WorkQueue(object):
|
|||
minutes.
|
||||
"""
|
||||
now = datetime.now()
|
||||
available_or_expired = ((QueueItem.available == True) |
|
||||
(QueueItem.processing_expires <= now))
|
||||
|
||||
name_match_query = '%s%%' % self._canonical_name([self.queue_name] +
|
||||
self.canonical_name_match_list)
|
||||
|
||||
with transaction_factory(db):
|
||||
avail = QueueItem.select().where(QueueItem.queue_name == self.queue_name,
|
||||
QueueItem.available_after <= now,
|
||||
available_or_expired,
|
||||
QueueItem.retries_remaining > 0)
|
||||
running = (QueueItem
|
||||
.select(QueueItem.queue_name)
|
||||
.where(QueueItem.available == False,
|
||||
QueueItem.processing_expires > now,
|
||||
QueueItem.queue_name ** name_match_query))
|
||||
|
||||
found = list(avail.limit(1).order_by(QueueItem.available_after))
|
||||
avail = QueueItem.select().where(QueueItem.queue_name ** name_match_query,
|
||||
QueueItem.available_after <= now,
|
||||
((QueueItem.available == True) |
|
||||
(QueueItem.processing_expires <= now)),
|
||||
QueueItem.retries_remaining > 0,
|
||||
~(QueueItem.queue_name << running))
|
||||
|
||||
found = list(avail.limit(1).order_by(QueueItem.id))
|
||||
|
||||
if found:
|
||||
item = found[0]
|
||||
|
@ -57,16 +75,24 @@ class WorkQueue(object):
|
|||
|
||||
return None
|
||||
|
||||
def complete(self, completed_item):
|
||||
@staticmethod
|
||||
def complete(completed_item):
|
||||
completed_item.delete_instance()
|
||||
|
||||
def incomplete(self, incomplete_item, retry_after=300):
|
||||
@staticmethod
|
||||
def incomplete(incomplete_item, retry_after=300):
|
||||
retry_date = datetime.now() + timedelta(seconds=retry_after)
|
||||
incomplete_item.available_after = retry_date
|
||||
incomplete_item.available = True
|
||||
incomplete_item.save()
|
||||
|
||||
@staticmethod
|
||||
def extend_processing(queue_item, seconds_from_now):
|
||||
new_expiration = datetime.now() + timedelta(seconds=seconds_from_now)
|
||||
queue_item.processing_expires = new_expiration
|
||||
queue_item.save()
|
||||
|
||||
image_diff_queue = WorkQueue('imagediff')
|
||||
dockerfile_build_queue = WorkQueue('dockerfilebuild3')
|
||||
webhook_queue = WorkQueue('webhook')
|
||||
|
||||
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'])
|
||||
dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME'])
|
||||
webhook_queue = WorkQueue(app.config['WEBHOOK_QUEUE_NAME'])
|
||||
|
|
|
@ -1,25 +1,43 @@
|
|||
import boto
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
import magic
|
||||
|
||||
from boto.s3.key import Key
|
||||
from uuid import uuid4
|
||||
from flask import url_for, request, send_file, make_response, abort
|
||||
from flask.views import View
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FakeUserfiles(object):
|
||||
def prepare_for_drop(self, mime_type):
|
||||
return ('http://fake/url', uuid4())
|
||||
|
||||
def store_file(self, file_like_obj, content_type):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_file_url(self, file_id, expires_in=300):
|
||||
return ('http://fake/url')
|
||||
|
||||
def get_file_checksum(self, file_id):
|
||||
return 'abcdefg'
|
||||
|
||||
|
||||
class S3FileWriteException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserRequestFiles(object):
|
||||
def __init__(self, s3_access_key, s3_secret_key, bucket_name):
|
||||
class S3Userfiles(object):
|
||||
def __init__(self, path, s3_access_key, s3_secret_key, bucket_name):
|
||||
self._initialized = False
|
||||
self._bucket_name = bucket_name
|
||||
self._access_key = s3_access_key
|
||||
self._secret_key = s3_secret_key
|
||||
self._prefix = 'userfiles'
|
||||
self._prefix = path
|
||||
self._s3_conn = None
|
||||
self._bucket = None
|
||||
|
||||
|
@ -70,3 +88,139 @@ class UserRequestFiles(object):
|
|||
full_key = os.path.join(self._prefix, file_id)
|
||||
k = self._bucket.lookup(full_key)
|
||||
return k.etag[1:-1][:7]
|
||||
|
||||
|
||||
class UserfilesHandlers(View):
|
||||
methods = ['GET', 'PUT']
|
||||
|
||||
def __init__(self, local_userfiles):
|
||||
self._userfiles = local_userfiles
|
||||
self._magic = magic.Magic(mime=True)
|
||||
|
||||
def get(self, file_id):
|
||||
path = self._userfiles.file_path(file_id)
|
||||
if not os.path.exists(path):
|
||||
abort(404)
|
||||
|
||||
logger.debug('Sending path: %s' % path)
|
||||
return send_file(path, mimetype=self._magic.from_file(path))
|
||||
|
||||
def put(self, file_id):
|
||||
input_stream = request.stream
|
||||
if request.headers.get('transfer-encoding') == 'chunked':
|
||||
# Careful, might work only with WSGI servers supporting chunked
|
||||
# encoding (Gunicorn)
|
||||
input_stream = request.environ['wsgi.input']
|
||||
|
||||
self._userfiles.store_stream(input_stream, file_id)
|
||||
|
||||
return make_response('Okay')
|
||||
|
||||
def dispatch_request(self, file_id):
|
||||
if request.method == 'GET':
|
||||
return self.get(file_id)
|
||||
elif request.method == 'PUT':
|
||||
return self.put(file_id)
|
||||
|
||||
|
||||
class LocalUserfiles(object):
|
||||
def __init__(self, app, path):
|
||||
self._root_path = path
|
||||
self._buffer_size = 64 * 1024 # 64 KB
|
||||
self._app = app
|
||||
|
||||
def _build_url_adapter(self):
|
||||
return self._app.url_map.bind(self._app.config['SERVER_HOSTNAME'],
|
||||
script_name=self._app.config['APPLICATION_ROOT'] or '/',
|
||||
url_scheme=self._app.config['PREFERRED_URL_SCHEME'])
|
||||
|
||||
def prepare_for_drop(self, mime_type):
|
||||
file_id = str(uuid4())
|
||||
with self._app.app_context() as ctx:
|
||||
ctx.url_adapter = self._build_url_adapter()
|
||||
return (url_for('userfiles_handlers', file_id=file_id, _external=True), file_id)
|
||||
|
||||
def file_path(self, file_id):
|
||||
if '..' in file_id or file_id.startswith('/'):
|
||||
raise RuntimeError('Invalid Filename')
|
||||
return os.path.join(self._root_path, file_id)
|
||||
|
||||
def store_stream(self, stream, file_id):
|
||||
path = self.file_path(file_id)
|
||||
dirname = os.path.dirname(path)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
with open(path, 'w') as to_write:
|
||||
while True:
|
||||
try:
|
||||
buf = stream.read(self._buffer_size)
|
||||
if not buf:
|
||||
break
|
||||
to_write.write(buf)
|
||||
except IOError:
|
||||
break
|
||||
|
||||
def store_file(self, file_like_obj, content_type):
|
||||
file_id = str(uuid4())
|
||||
|
||||
# Rewind the file to match what s3 does
|
||||
file_like_obj.seek(0, os.SEEK_SET)
|
||||
|
||||
self.store_stream(file_like_obj, file_id)
|
||||
return file_id
|
||||
|
||||
def get_file_url(self, file_id, expires_in=300):
|
||||
with self._app.app_context() as ctx:
|
||||
ctx.url_adapter = self._build_url_adapter()
|
||||
return url_for('userfiles_handlers', file_id=file_id, _external=True)
|
||||
|
||||
def get_file_checksum(self, file_id):
|
||||
path = self.file_path(file_id)
|
||||
sha_hash = hashlib.sha256()
|
||||
with open(path, 'r') as to_hash:
|
||||
while True:
|
||||
buf = to_hash.read(self._buffer_size)
|
||||
if not buf:
|
||||
break
|
||||
sha_hash.update(buf)
|
||||
return sha_hash.hexdigest()[:7]
|
||||
|
||||
|
||||
class Userfiles(object):
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
if app is not None:
|
||||
self.state = self.init_app(app)
|
||||
else:
|
||||
self.state = None
|
||||
|
||||
def init_app(self, app):
|
||||
storage_type = app.config.get('USERFILES_TYPE', 'LocalUserfiles')
|
||||
path = app.config.get('USERFILES_PATH', '')
|
||||
|
||||
if storage_type == 'LocalUserfiles':
|
||||
userfiles = LocalUserfiles(app, path)
|
||||
app.add_url_rule('/userfiles/<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 wrapper(api_resource):
|
||||
if not api_resource:
|
||||
return None
|
||||
|
||||
api.add_resource(api_resource, *urls, **kwargs)
|
||||
return api_resource
|
||||
return wrapper
|
||||
|
||||
|
||||
def show_if(value):
|
||||
def f(inner):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
return inner
|
||||
return f
|
||||
|
||||
|
||||
def hide_if(value):
|
||||
def f(inner):
|
||||
if value:
|
||||
return None
|
||||
|
||||
return inner
|
||||
return f
|
||||
|
||||
|
||||
def truthy_bool(param):
|
||||
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
|
||||
|
||||
|
@ -103,6 +124,9 @@ def format_date(date):
|
|||
|
||||
def add_method_metadata(name, value):
|
||||
def modifier(func):
|
||||
if func is None:
|
||||
return None
|
||||
|
||||
if '__api_metadata' not in dir(func):
|
||||
func.__api_metadata = {}
|
||||
func.__api_metadata[name] = value
|
||||
|
@ -111,11 +135,15 @@ def add_method_metadata(name, value):
|
|||
|
||||
|
||||
def method_metadata(func, name):
|
||||
if func is None:
|
||||
return None
|
||||
|
||||
if '__api_metadata' in dir(func):
|
||||
return func.__api_metadata.get(name, None)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
nickname = partial(add_method_metadata, 'nickname')
|
||||
related_user_resource = partial(add_method_metadata, 'related_user_resource')
|
||||
internal_only = add_method_metadata('internal', True)
|
||||
|
@ -274,6 +302,7 @@ import endpoints.api.repository
|
|||
import endpoints.api.repotoken
|
||||
import endpoints.api.robot
|
||||
import endpoints.api.search
|
||||
import endpoints.api.superuser
|
||||
import endpoints.api.tag
|
||||
import endpoints.api.team
|
||||
import endpoints.api.trigger
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import stripe
|
||||
|
||||
from flask import request
|
||||
|
||||
from app import billing
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||
require_user_admin)
|
||||
require_user_admin, show_if, hide_if)
|
||||
from endpoints.api.subscribe import subscribe, subscription_view
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from data import model
|
||||
from data.plans import PLANS
|
||||
from data.billing import PLANS
|
||||
|
||||
import features
|
||||
|
||||
def carderror_response(e):
|
||||
return {'carderror': e.message}, 402
|
||||
|
@ -22,7 +23,7 @@ def get_card(user):
|
|||
}
|
||||
|
||||
if user.stripe_id:
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
if cus and cus.default_card:
|
||||
# Find the default card.
|
||||
default_card = None
|
||||
|
@ -43,7 +44,7 @@ def get_card(user):
|
|||
|
||||
def set_card(user, token):
|
||||
if user.stripe_id:
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
if cus:
|
||||
try:
|
||||
cus.card = token
|
||||
|
@ -72,13 +73,14 @@ def get_invoices(customer_id):
|
|||
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||
}
|
||||
|
||||
invoices = stripe.Invoice.all(customer=customer_id, count=12)
|
||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||
return {
|
||||
'invoices': [invoice_view(i) for i in invoices.data]
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/plans/')
|
||||
@show_if(features.BILLING)
|
||||
class ListPlans(ApiResource):
|
||||
""" Resource for listing the available plans. """
|
||||
@nickname('listPlans')
|
||||
|
@ -91,6 +93,7 @@ class ListPlans(ApiResource):
|
|||
|
||||
@resource('/v1/user/card')
|
||||
@internal_only
|
||||
@show_if(features.BILLING)
|
||||
class UserCard(ApiResource):
|
||||
""" Resource for managing a user's credit card. """
|
||||
schemas = {
|
||||
|
@ -132,6 +135,7 @@ class UserCard(ApiResource):
|
|||
@resource('/v1/organization/<orgname>/card')
|
||||
@internal_only
|
||||
@related_user_resource(UserCard)
|
||||
@show_if(features.BILLING)
|
||||
class OrganizationCard(ApiResource):
|
||||
""" Resource for managing an organization's credit card. """
|
||||
schemas = {
|
||||
|
@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
|
|||
|
||||
@resource('/v1/user/plan')
|
||||
@internal_only
|
||||
@show_if(features.BILLING)
|
||||
class UserPlan(ApiResource):
|
||||
""" Resource for managing a user's subscription. """
|
||||
schemas = {
|
||||
|
@ -216,16 +221,19 @@ class UserPlan(ApiResource):
|
|||
@nickname('getUserSubscription')
|
||||
def get(self):
|
||||
""" Fetch any existing subscription for the user. """
|
||||
cus = None
|
||||
user = get_authenticated_user()
|
||||
private_repos = model.get_private_repo_count(user.username)
|
||||
|
||||
if user.stripe_id:
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
||||
return {
|
||||
'hasSubscription': False,
|
||||
'isExistingCustomer': cus is not None,
|
||||
'plan': 'free',
|
||||
'usedPrivateRepos': private_repos,
|
||||
}
|
||||
|
@ -234,6 +242,7 @@ class UserPlan(ApiResource):
|
|||
@resource('/v1/organization/<orgname>/plan')
|
||||
@internal_only
|
||||
@related_user_resource(UserPlan)
|
||||
@show_if(features.BILLING)
|
||||
class OrganizationPlan(ApiResource):
|
||||
""" Resource for managing a org's subscription. """
|
||||
schemas = {
|
||||
|
@ -274,17 +283,20 @@ class OrganizationPlan(ApiResource):
|
|||
@nickname('getOrgSubscription')
|
||||
def get(self, orgname):
|
||||
""" Fetch any existing subscription for the org. """
|
||||
cus = None
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
private_repos = model.get_private_repo_count(orgname)
|
||||
organization = model.get_organization(orgname)
|
||||
if organization.stripe_id:
|
||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
||||
return {
|
||||
'hasSubscription': False,
|
||||
'isExistingCustomer': cus is not None,
|
||||
'plan': 'free',
|
||||
'usedPrivateRepos': private_repos,
|
||||
}
|
||||
|
@ -294,6 +306,7 @@ class OrganizationPlan(ApiResource):
|
|||
|
||||
@resource('/v1/user/invoices')
|
||||
@internal_only
|
||||
@show_if(features.BILLING)
|
||||
class UserInvoiceList(ApiResource):
|
||||
""" Resource for listing a user's invoices. """
|
||||
@require_user_admin
|
||||
|
@ -310,6 +323,7 @@ class UserInvoiceList(ApiResource):
|
|||
@resource('/v1/organization/<orgname>/invoices')
|
||||
@internal_only
|
||||
@related_user_resource(UserInvoiceList)
|
||||
@show_if(features.BILLING)
|
||||
class OrgnaizationInvoiceList(ApiResource):
|
||||
""" Resource for listing an orgnaization's invoices. """
|
||||
@nickname('listOrgInvoices')
|
||||
|
@ -323,4 +337,4 @@ class OrgnaizationInvoiceList(ApiResource):
|
|||
|
||||
return get_invoices(organization.stripe_id)
|
||||
|
||||
raise Unauthorized()
|
||||
raise Unauthorized()
|
||||
|
|
|
@ -3,7 +3,7 @@ import json
|
|||
|
||||
from flask import request
|
||||
|
||||
from app import app
|
||||
from app import app, userfiles as user_files
|
||||
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||
require_repo_read, require_repo_write, validate_json_request,
|
||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
|
||||
|
@ -17,7 +17,6 @@ from util.names import parse_robot_username
|
|||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
user_files = app.config['USERFILES']
|
||||
build_logs = app.config['BUILDLOGS']
|
||||
|
||||
|
||||
|
|
|
@ -23,13 +23,12 @@ TYPE_CONVERTER = {
|
|||
int: 'integer',
|
||||
}
|
||||
|
||||
URL_SCHEME = app.config['URL_SCHEME']
|
||||
URL_HOST = app.config['URL_HOST']
|
||||
PREFERRED_URL_SCHEME = app.config['PREFERRED_URL_SCHEME']
|
||||
SERVER_HOSTNAME = app.config['SERVER_HOSTNAME']
|
||||
|
||||
|
||||
def fully_qualified_name(method_view_class):
|
||||
inst = method_view_class()
|
||||
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
|
||||
return '%s.%s' % (method_view_class.__module__, method_view_class.__name__)
|
||||
|
||||
|
||||
def swagger_route_data(include_internal=False, compact=False):
|
||||
|
@ -143,7 +142,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
swagger_data = {
|
||||
'apiVersion': 'v1',
|
||||
'swaggerVersion': '1.2',
|
||||
'basePath': '%s://%s' % (URL_SCHEME, URL_HOST),
|
||||
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
|
||||
'resourcePath': '/',
|
||||
'info': {
|
||||
'title': 'Quay.io API',
|
||||
|
@ -160,7 +159,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
"implicit": {
|
||||
"tokenName": "access_token",
|
||||
"loginEndpoint": {
|
||||
"url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST),
|
||||
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,16 +2,13 @@ import json
|
|||
|
||||
from collections import defaultdict
|
||||
|
||||
from app import app
|
||||
from app import storage as store
|
||||
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
||||
format_date, NotFound)
|
||||
from data import model
|
||||
from util.cache import cache_control_flask_restful
|
||||
|
||||
|
||||
store = app.config['STORAGE']
|
||||
|
||||
|
||||
def image_view(image):
|
||||
extended_props = image
|
||||
if image.storage and image.storage.id:
|
||||
|
|
|
@ -29,8 +29,7 @@ def log_view(log):
|
|||
return view
|
||||
|
||||
|
||||
def get_logs(namespace, start_time, end_time, performer_name=None,
|
||||
repository=None):
|
||||
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
|
||||
performer = None
|
||||
if performer_name:
|
||||
performer = model.get_user(performer_name)
|
||||
|
@ -54,8 +53,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None,
|
|||
if not end_time:
|
||||
end_time = datetime.today()
|
||||
|
||||
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
|
||||
repository=repository)
|
||||
logs = model.list_logs(start_time, end_time, performer=performer, repository=repository,
|
||||
namespace=namespace)
|
||||
return {
|
||||
'start_time': format_date(start_time),
|
||||
'end_time': format_date(end_time),
|
||||
|
@ -80,7 +79,7 @@ class RepositoryLogs(RepositoryParamResource):
|
|||
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
return get_logs(namespace, start_time, end_time, repository=repo)
|
||||
return get_logs(start_time, end_time, repository=repo, namespace=namespace)
|
||||
|
||||
|
||||
@resource('/v1/user/logs')
|
||||
|
@ -100,7 +99,7 @@ class UserLogs(ApiResource):
|
|||
end_time = args['endtime']
|
||||
|
||||
user = get_authenticated_user()
|
||||
return get_logs(user.username, start_time, end_time, performer_name=performer_name)
|
||||
return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/logs')
|
||||
|
@ -121,6 +120,6 @@ class OrgLogs(ApiResource):
|
|||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
|
||||
return get_logs(orgname, start_time, end_time, performer_name=performer_name)
|
||||
return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name)
|
||||
|
||||
raise Unauthorized()
|
||||
raise Unauthorized()
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import logging
|
||||
import stripe
|
||||
|
||||
from flask import request
|
||||
|
||||
from app import billing as stripe
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||
require_user_admin, log_action)
|
||||
require_user_admin, log_action, show_if)
|
||||
from endpoints.api.team import team_view
|
||||
from endpoints.api.user import User, PrivateRepositories
|
||||
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
||||
CreateRepositoryPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from data import model
|
||||
from data.plans import get_plan
|
||||
from data.billing import get_plan
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
import features
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -163,6 +165,7 @@ class Organization(ApiResource):
|
|||
@resource('/v1/organization/<orgname>/private')
|
||||
@internal_only
|
||||
@related_user_resource(PrivateRepositories)
|
||||
@show_if(features.BILLING)
|
||||
class OrgPrivateRepositories(ApiResource):
|
||||
""" Custom verb to compute whether additional private repositories are available. """
|
||||
@nickname('getOrganizationPrivateAllowed')
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import logging
|
||||
import stripe
|
||||
|
||||
from app import billing
|
||||
from endpoints.api import request_error, log_action, NotFound
|
||||
from endpoints.common import check_repository_usage
|
||||
from data import model
|
||||
from data.plans import PLANS
|
||||
from data.billing import PLANS
|
||||
|
||||
import features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -15,15 +17,24 @@ def carderror_response(exc):
|
|||
|
||||
|
||||
def subscription_view(stripe_subscription, used_repos):
|
||||
return {
|
||||
view = {
|
||||
'hasSubscription': True,
|
||||
'isExistingCustomer': True,
|
||||
'currentPeriodStart': stripe_subscription.current_period_start,
|
||||
'currentPeriodEnd': stripe_subscription.current_period_end,
|
||||
'plan': stripe_subscription.plan.id,
|
||||
'usedPrivateRepos': used_repos,
|
||||
'trialStart': stripe_subscription.trial_start,
|
||||
'trialEnd': stripe_subscription.trial_end
|
||||
}
|
||||
|
||||
return view
|
||||
|
||||
|
||||
def subscribe(user, plan, token, require_business_plan):
|
||||
if not features.BILLING:
|
||||
return
|
||||
|
||||
plan_found = None
|
||||
for plan_obj in PLANS:
|
||||
if plan_obj['stripeId'] == plan:
|
||||
|
@ -56,7 +67,7 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
card = token
|
||||
|
||||
try:
|
||||
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
||||
cus = billing.Customer.create(email=user.email, plan=plan, card=card)
|
||||
user.stripe_id = cus.id
|
||||
user.save()
|
||||
check_repository_usage(user, plan_found)
|
||||
|
@ -69,7 +80,7 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
|
||||
else:
|
||||
# Change the plan
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
|
||||
if plan_found['price'] == 0:
|
||||
if cus.subscription is not None:
|
||||
|
|
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,
|
||||
RepositoryParamResource, log_action, NotFound)
|
||||
from flask import request
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||
RepositoryParamResource, log_action, NotFound, validate_json_request)
|
||||
from endpoints.api.image import image_view
|
||||
from data import model
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
@ -8,8 +10,54 @@ from auth.auth_context import get_authenticated_user
|
|||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||
class RepositoryTag(RepositoryParamResource):
|
||||
""" Resource for managing repository tags. """
|
||||
schemas = {
|
||||
'MoveTag': {
|
||||
'id': 'MoveTag',
|
||||
'type': 'object',
|
||||
'description': 'Description of to which image a new or existing tag should point',
|
||||
'required': [
|
||||
'image',
|
||||
],
|
||||
'properties': {
|
||||
'image': {
|
||||
'type': 'string',
|
||||
'description': 'Image identifier to which the tag should point',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_repo_admin
|
||||
@require_repo_write
|
||||
@nickname('changeTagImage')
|
||||
@validate_json_request('MoveTag')
|
||||
def put(self, namespace, repository, tag):
|
||||
""" Change which image a tag points to or create a new tag."""
|
||||
image_id = request.get_json()['image']
|
||||
image = model.get_repo_image(namespace, repository, image_id)
|
||||
if not image:
|
||||
raise NotFound()
|
||||
|
||||
original_image_id = None
|
||||
try:
|
||||
original_tag_image = model.get_tag_image(namespace, repository, tag)
|
||||
if original_tag_image:
|
||||
original_image_id = original_tag_image.docker_image_id
|
||||
except model.DataModelException:
|
||||
# This is a new tag.
|
||||
pass
|
||||
|
||||
model.create_or_update_tag(namespace, repository, tag, image_id)
|
||||
model.garbage_collect_repository(namespace, repository)
|
||||
|
||||
username = get_authenticated_user().username
|
||||
log_action('move_tag' if original_image_id else 'create_tag', namespace,
|
||||
{ 'username': username, 'repo': repository, 'tag': tag,
|
||||
'image': image_id, 'original_image': original_image_id },
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return 'Updated', 201
|
||||
|
||||
@require_repo_write
|
||||
@nickname('deleteFullTag')
|
||||
def delete(self, namespace, repository, tag):
|
||||
""" Delete the specified repository tag. """
|
||||
|
|
|
@ -205,9 +205,8 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
trigger.repository.name)
|
||||
path = url_for('webhooks.build_trigger_webhook',
|
||||
repository=repository_path, trigger_uuid=trigger.uuid)
|
||||
authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token',
|
||||
token.code, app.config['URL_HOST'],
|
||||
path)
|
||||
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code,
|
||||
app.config['SERVER_HOSTNAME'], path)
|
||||
|
||||
final_config = handler.activate(trigger.uuid, authed_url,
|
||||
trigger.auth_token, new_config_dict)
|
||||
|
@ -294,7 +293,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
}
|
||||
|
||||
# Check to see if the base image lives in Quay.
|
||||
quay_registry_prefix = '%s/' % (app.config['URL_HOST'])
|
||||
quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME'])
|
||||
|
||||
if not base_image.startswith(quay_registry_prefix):
|
||||
return {
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import logging
|
||||
import stripe
|
||||
import json
|
||||
|
||||
from flask import request
|
||||
from flask.ext.login import logout_user
|
||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||
|
||||
from app import app
|
||||
from app import app, billing as stripe
|
||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||
log_action, internal_only, NotFound, require_user_admin,
|
||||
InvalidToken, require_scope, format_date)
|
||||
InvalidToken, require_scope, format_date, hide_if, show_if)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from data import model
|
||||
from data.plans import get_plan
|
||||
from data.billing import get_plan
|
||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||
UserAdminPermission, UserReadPermission)
|
||||
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from util.gravatar import compute_hash
|
||||
from util.email import (send_confirmation_email, send_recovery_email,
|
||||
send_change_email)
|
||||
|
||||
import features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -65,6 +65,11 @@ def user_view(user):
|
|||
'preferred_namespace': not (user.stripe_id is None),
|
||||
})
|
||||
|
||||
if features.SUPER_USERS:
|
||||
user_response.update({
|
||||
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
|
||||
})
|
||||
|
||||
return user_response
|
||||
|
||||
|
||||
|
@ -193,6 +198,7 @@ class User(ApiResource):
|
|||
|
||||
@resource('/v1/user/private')
|
||||
@internal_only
|
||||
@show_if(features.BILLING)
|
||||
class PrivateRepositories(ApiResource):
|
||||
""" Operations dealing with the available count of private repositories. """
|
||||
@require_user_admin
|
||||
|
@ -248,8 +254,7 @@ class ConvertToOrganization(ApiResource):
|
|||
'description': 'Information required to convert a user to an organization.',
|
||||
'required': [
|
||||
'adminUser',
|
||||
'adminPassword',
|
||||
'plan',
|
||||
'adminPassword'
|
||||
],
|
||||
'properties': {
|
||||
'adminUser': {
|
||||
|
@ -262,7 +267,7 @@ class ConvertToOrganization(ApiResource):
|
|||
},
|
||||
'plan': {
|
||||
'type': 'string',
|
||||
'description': 'The plan to which the organizatino should be subscribed',
|
||||
'description': 'The plan to which the organization should be subscribed',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -289,8 +294,9 @@ class ConvertToOrganization(ApiResource):
|
|||
message='The admin user credentials are not valid')
|
||||
|
||||
# Subscribe the organization to the new plan.
|
||||
plan = convert_data['plan']
|
||||
subscribe(user, plan, None, True) # Require business plans
|
||||
if features.BILLING:
|
||||
plan = convert_data.get('plan', 'free')
|
||||
subscribe(user, plan, None, True) # Require business plans
|
||||
|
||||
# Convert the user to an organization.
|
||||
model.convert_user_to_organization(user, model.get_user(admin_username))
|
||||
|
|
|
@ -3,14 +3,15 @@ import logging
|
|||
from flask import request, redirect, url_for, Blueprint
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from endpoints.common import render_page_template, common_login
|
||||
from app import app, mixpanel
|
||||
from endpoints.common import render_page_template, common_login, route_show_if
|
||||
from app import app, analytics
|
||||
from data import model
|
||||
from util.names import parse_repository_name
|
||||
from util.http import abort
|
||||
from auth.permissions import AdministerRepositoryPermission
|
||||
from auth.auth import require_session_login
|
||||
|
||||
import features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,11 +21,11 @@ client = app.config['HTTPCLIENT']
|
|||
callback = Blueprint('callback', __name__)
|
||||
|
||||
|
||||
def exchange_github_code_for_token(code):
|
||||
def exchange_github_code_for_token(code, for_login=True):
|
||||
code = request.args.get('code')
|
||||
payload = {
|
||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
||||
'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'],
|
||||
'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'],
|
||||
'code': code,
|
||||
}
|
||||
headers = {
|
||||
|
@ -48,6 +49,7 @@ def get_github_user(token):
|
|||
|
||||
|
||||
@callback.route('/github/callback', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_LOGIN)
|
||||
def github_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
|
@ -83,13 +85,13 @@ def github_oauth_callback():
|
|||
to_login = model.create_federated_user(username, found_email, 'github',
|
||||
github_id)
|
||||
|
||||
# Success, tell mixpanel
|
||||
mixpanel.track(to_login.username, 'register', {'service': 'github'})
|
||||
# Success, tell analytics
|
||||
analytics.track(to_login.username, 'register', {'service': 'github'})
|
||||
|
||||
state = request.args.get('state', None)
|
||||
if state:
|
||||
logger.debug('Aliasing with state: %s' % state)
|
||||
mixpanel.alias(to_login.username, state)
|
||||
analytics.alias(to_login.username, state)
|
||||
|
||||
except model.DataModelException, ex:
|
||||
return render_page_template('githuberror.html', error_message=ex.message)
|
||||
|
@ -101,6 +103,7 @@ def github_oauth_callback():
|
|||
|
||||
|
||||
@callback.route('/github/callback/attach', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_LOGIN)
|
||||
@require_session_login
|
||||
def github_oauth_attach():
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
|
@ -117,7 +120,7 @@ def github_oauth_attach():
|
|||
def attach_github_build_trigger(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
token = exchange_github_code_for_token(request.args.get('code'), for_login=False)
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
||||
|
|
|
@ -3,7 +3,7 @@ import urlparse
|
|||
import json
|
||||
import string
|
||||
|
||||
from flask import make_response, render_template, request
|
||||
from flask import make_response, render_template, request, abort
|
||||
from flask.ext.login import login_user, UserMixin
|
||||
from flask.ext.principal import identity_changed
|
||||
from random import SystemRandom
|
||||
|
@ -15,7 +15,10 @@ from auth.permissions import QuayDeferredPermissionUser
|
|||
from auth import scopes
|
||||
from endpoints.api.discovery import swagger_route_data
|
||||
from werkzeug.routing import BaseConverter
|
||||
from functools import wraps
|
||||
from config import getFrontendVisibleConfig
|
||||
|
||||
import features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,6 +30,29 @@ class RepoPathConverter(BaseConverter):
|
|||
|
||||
app.url_map.converters['repopath'] = RepoPathConverter
|
||||
|
||||
def route_show_if(value):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not value:
|
||||
abort(404)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def route_hide_if(value):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if value:
|
||||
abort(404)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def get_route_data():
|
||||
global route_data
|
||||
|
@ -89,9 +115,52 @@ def random_string():
|
|||
random = SystemRandom()
|
||||
return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(8)])
|
||||
|
||||
def list_files(path, extension):
|
||||
import os
|
||||
def matches(f):
|
||||
return os.path.splitext(f)[1] == '.' + extension
|
||||
|
||||
def join_path(dp, f):
|
||||
# Remove the static/ prefix. It is added in the template.
|
||||
return os.path.join(dp, f)[len('static/'):]
|
||||
|
||||
filepath = 'static/' + path
|
||||
return [join_path(dp, f) for dp, dn, files in os.walk(filepath) for f in files if matches(f)]
|
||||
|
||||
def render_page_template(name, **kwargs):
|
||||
if app.config.get('DEBUGGING', False):
|
||||
# If DEBUGGING is enabled, then we load the full set of individual JS and CSS files
|
||||
# from the file system.
|
||||
library_styles = list_files('lib', 'css')
|
||||
main_styles = list_files('css', 'css')
|
||||
library_scripts = list_files('lib', 'js')
|
||||
main_scripts = list_files('js', 'js')
|
||||
cache_buster = 'debugging'
|
||||
|
||||
file_lists = [library_styles, main_styles, library_scripts, main_scripts]
|
||||
for file_list in file_lists:
|
||||
file_list.sort()
|
||||
else:
|
||||
library_styles = []
|
||||
main_styles = ['dist/quay-frontend.css']
|
||||
library_scripts = []
|
||||
main_scripts = ['dist/quay-frontend.min.js']
|
||||
cache_buster = random_string()
|
||||
|
||||
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()),
|
||||
cache_buster=random_string(), **kwargs))
|
||||
main_styles=main_styles,
|
||||
library_styles=library_styles,
|
||||
main_scripts=main_scripts,
|
||||
library_scripts=library_scripts,
|
||||
feature_set=json.dumps(features.get_features()),
|
||||
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
|
||||
show_chat=features.OLARK_CHAT,
|
||||
cache_buster=cache_buster,
|
||||
**kwargs))
|
||||
|
||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||
return resp
|
||||
|
||||
|
@ -125,7 +194,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
|||
dockerfile_id, build_name,
|
||||
trigger, pull_robot_name = pull_robot_name)
|
||||
|
||||
dockerfile_build_queue.put(json.dumps({
|
||||
dockerfile_build_queue.put([repository.namespace, repository.name], json.dumps({
|
||||
'build_uuid': build_request.uuid,
|
||||
'namespace': repository.namespace,
|
||||
'repository': repository.name,
|
||||
|
|
|
@ -9,7 +9,7 @@ from collections import OrderedDict
|
|||
from data import model
|
||||
from data.model import oauth
|
||||
from data.queue import webhook_queue
|
||||
from app import mixpanel, app
|
||||
from app import analytics, app
|
||||
from auth.auth import process_auth
|
||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||
from util.names import parse_repository_name
|
||||
|
@ -21,6 +21,7 @@ from util.http import abort
|
|||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
profile = logging.getLogger('application.profiler')
|
||||
|
||||
index = Blueprint('index', __name__)
|
||||
|
||||
|
@ -112,9 +113,15 @@ def create_user():
|
|||
|
||||
else:
|
||||
# New user case
|
||||
profile.debug('Creating user')
|
||||
new_user = model.create_user(username, password, user_data['email'])
|
||||
|
||||
profile.debug('Creating email code for user')
|
||||
code = model.create_confirm_email_code(new_user)
|
||||
|
||||
profile.debug('Sending email code to user')
|
||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||
|
||||
return make_response('Created', 201)
|
||||
|
||||
|
||||
|
@ -149,12 +156,12 @@ def update_user(username):
|
|||
update_request = request.get_json()
|
||||
|
||||
if 'password' in update_request:
|
||||
logger.debug('Updating user password.')
|
||||
profile.debug('Updating user password')
|
||||
model.change_password(get_authenticated_user(),
|
||||
update_request['password'])
|
||||
|
||||
if 'email' in update_request:
|
||||
logger.debug('Updating user email')
|
||||
profile.debug('Updating user email')
|
||||
model.update_email(get_authenticated_user(), update_request['email'])
|
||||
|
||||
return jsonify({
|
||||
|
@ -170,9 +177,13 @@ def update_user(username):
|
|||
@parse_repository_name
|
||||
@generate_headers(role='write')
|
||||
def create_repository(namespace, repository):
|
||||
profile.debug('Parsing image descriptions')
|
||||
image_descriptions = json.loads(request.data)
|
||||
|
||||
profile.debug('Looking up repository')
|
||||
repo = model.get_repository(namespace, repository)
|
||||
|
||||
profile.debug('Repository looked up')
|
||||
if not repo and get_authenticated_user() is None:
|
||||
logger.debug('Attempt to create new repository without user auth.')
|
||||
abort(401,
|
||||
|
@ -196,11 +207,11 @@ def create_repository(namespace, repository):
|
|||
issue='no-create-permission',
|
||||
namespace=namespace)
|
||||
|
||||
logger.debug('Creaing repository with owner: %s' %
|
||||
get_authenticated_user().username)
|
||||
profile.debug('Creaing repository with owner: %s', get_authenticated_user().username)
|
||||
repo = model.create_repository(namespace, repository,
|
||||
get_authenticated_user())
|
||||
|
||||
profile.debug('Determining added images')
|
||||
added_images = OrderedDict([(desc['id'], desc)
|
||||
for desc in image_descriptions])
|
||||
new_repo_images = dict(added_images)
|
||||
|
@ -209,12 +220,15 @@ def create_repository(namespace, repository):
|
|||
if existing.docker_image_id in new_repo_images:
|
||||
added_images.pop(existing.docker_image_id)
|
||||
|
||||
profile.debug('Creating/Linking necessary images')
|
||||
username = get_authenticated_user() and get_authenticated_user().username
|
||||
translations = {}
|
||||
for image_description in added_images.values():
|
||||
model.find_create_or_link_image(image_description['id'], repo, username,
|
||||
translations)
|
||||
|
||||
|
||||
profile.debug('Created images')
|
||||
response = make_response('Created', 201)
|
||||
|
||||
extra_params = {
|
||||
|
@ -227,7 +241,7 @@ def create_repository(namespace, repository):
|
|||
}
|
||||
|
||||
if get_validated_oauth_token():
|
||||
mixpanel.track(username, 'push_repo', extra_params)
|
||||
analytics.track(username, 'push_repo', extra_params)
|
||||
|
||||
oauth_token = get_validated_oauth_token()
|
||||
metadata['oauth_token_id'] = oauth_token.id
|
||||
|
@ -236,7 +250,7 @@ def create_repository(namespace, repository):
|
|||
elif get_authenticated_user():
|
||||
username = get_authenticated_user().username
|
||||
|
||||
mixpanel.track(username, 'push_repo', extra_params)
|
||||
analytics.track(username, 'push_repo', extra_params)
|
||||
metadata['username'] = username
|
||||
|
||||
# Mark that the user has started pushing the repo.
|
||||
|
@ -250,7 +264,7 @@ def create_repository(namespace, repository):
|
|||
event.publish_event_data('docker-cli', user_data)
|
||||
|
||||
elif get_validated_token():
|
||||
mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
|
||||
analytics.track(get_validated_token().code, 'push_repo', extra_params)
|
||||
metadata['token'] = get_validated_token().friendly_name
|
||||
metadata['token_code'] = get_validated_token().code
|
||||
|
||||
|
@ -268,21 +282,23 @@ def update_images(namespace, repository):
|
|||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
|
||||
if permission.can():
|
||||
profile.debug('Looking up repository')
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
# Make sure the repo actually exists.
|
||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||
|
||||
profile.debug('Parsing image data')
|
||||
image_with_checksums = json.loads(request.data)
|
||||
|
||||
updated_tags = {}
|
||||
for image in image_with_checksums:
|
||||
logger.debug('Setting checksum for image id: %s to %s' %
|
||||
(image['id'], image['checksum']))
|
||||
profile.debug('Setting checksum for image id: %s to %s', image['id'], image['checksum'])
|
||||
updated_tags[image['Tag']] = image['id']
|
||||
model.set_image_checksum(image['id'], repo, image['checksum'])
|
||||
|
||||
if get_authenticated_user():
|
||||
profile.debug('Publishing push event')
|
||||
username = get_authenticated_user().username
|
||||
|
||||
# Mark that the user has pushed the repo.
|
||||
|
@ -295,15 +311,18 @@ def update_images(namespace, repository):
|
|||
event = app.config['USER_EVENTS'].get_event(username)
|
||||
event.publish_event_data('docker-cli', user_data)
|
||||
|
||||
profile.debug('GCing repository')
|
||||
num_removed = model.garbage_collect_repository(namespace, repository)
|
||||
|
||||
# Generate a job for each webhook that has been added to this repo
|
||||
profile.debug('Adding webhooks for repository')
|
||||
|
||||
webhooks = model.list_webhooks(namespace, repository)
|
||||
for webhook in webhooks:
|
||||
webhook_data = json.loads(webhook.parameters)
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
logger.debug('Creating webhook for repository \'%s\' for url \'%s\'' %
|
||||
(repo_string, webhook_data['url']))
|
||||
profile.debug('Creating webhook for repository \'%s\' for url \'%s\'',
|
||||
repo_string, webhook_data['url'])
|
||||
webhook_data['payload'] = {
|
||||
'repository': repo_string,
|
||||
'namespace': namespace,
|
||||
|
@ -315,7 +334,7 @@ def update_images(namespace, repository):
|
|||
'pushed_image_count': len(image_with_checksums),
|
||||
'pruned_image_count': num_removed,
|
||||
}
|
||||
webhook_queue.put(json.dumps(webhook_data))
|
||||
webhook_queue.put([namespace, repository], json.dumps(webhook_data))
|
||||
|
||||
return make_response('Updated', 204)
|
||||
|
||||
|
@ -330,14 +349,17 @@ def get_repository_images(namespace, repository):
|
|||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
||||
# TODO invalidate token?
|
||||
profile.debug('Looking up public status of repository')
|
||||
is_public = model.repository_is_public(namespace, repository)
|
||||
if permission.can() or is_public:
|
||||
# We can't rely on permissions to tell us if a repo exists anymore
|
||||
profile.debug('Looking up repository')
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||
|
||||
all_images = []
|
||||
profile.debug('Retrieving repository images')
|
||||
for image in model.get_repository_images(namespace, repository):
|
||||
new_image_view = {
|
||||
'id': image.docker_image_id,
|
||||
|
@ -345,6 +367,7 @@ def get_repository_images(namespace, repository):
|
|||
}
|
||||
all_images.append(new_image_view)
|
||||
|
||||
profile.debug('Building repository image response')
|
||||
resp = make_response(json.dumps(all_images), 200)
|
||||
resp.mimetype = 'application/json'
|
||||
|
||||
|
@ -353,6 +376,7 @@ def get_repository_images(namespace, repository):
|
|||
'namespace': namespace,
|
||||
}
|
||||
|
||||
profile.debug('Logging the pull to Mixpanel and the log system')
|
||||
if get_validated_oauth_token():
|
||||
oauth_token = get_validated_oauth_token()
|
||||
metadata['oauth_token_id'] = oauth_token.id
|
||||
|
@ -374,7 +398,7 @@ def get_repository_images(namespace, repository):
|
|||
'repository': '%s/%s' % (namespace, repository),
|
||||
}
|
||||
|
||||
mixpanel.track(pull_username, 'pull_repo', extra_params)
|
||||
analytics.track(pull_username, 'pull_repo', extra_params)
|
||||
model.log_action('pull_repo', namespace,
|
||||
performer=get_authenticated_user(),
|
||||
ip=request.remote_addr, metadata=metadata,
|
||||
|
@ -408,4 +432,5 @@ def get_search():
|
|||
def ping():
|
||||
response = make_response('true', 200)
|
||||
response.headers['X-Docker-Registry-Version'] = '0.6.0'
|
||||
response.headers['X-Docker-Registry-Standalone'] = '0'
|
||||
return response
|
||||
|
|
|
@ -9,7 +9,7 @@ from time import time
|
|||
|
||||
from data.queue import image_diff_queue
|
||||
|
||||
from app import app
|
||||
from app import storage as store
|
||||
from auth.auth import process_auth, extract_namespace_repo_from_session
|
||||
from util import checksums, changes
|
||||
from util.http import abort
|
||||
|
@ -17,11 +17,11 @@ from auth.permissions import (ReadRepositoryPermission,
|
|||
ModifyRepositoryPermission)
|
||||
from data import model
|
||||
|
||||
|
||||
registry = Blueprint('registry', __name__)
|
||||
|
||||
store = app.config['STORAGE']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
profile = logging.getLogger('application.profiler')
|
||||
|
||||
class SocketReader(object):
|
||||
def __init__(self, fp):
|
||||
|
@ -40,16 +40,35 @@ class SocketReader(object):
|
|||
return buf
|
||||
|
||||
|
||||
def image_is_uploading(namespace, repository, image_id, repo_image):
|
||||
if repo_image and repo_image.storage and repo_image.storage.uploading is not None:
|
||||
return repo_image.storage.uploading
|
||||
|
||||
logger.warning('Setting legacy upload flag')
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id, uuid)
|
||||
return store.exists(mark_path)
|
||||
|
||||
|
||||
def mark_upload_complete(namespace, repository, image_id, repo_image):
|
||||
if repo_image and repo_image.storage and repo_image.storage.uploading is not None:
|
||||
repo_image.storage.uploading = False
|
||||
repo_image.storage.save()
|
||||
else:
|
||||
logger.warning('Removing legacy upload flag')
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id, uuid)
|
||||
if store.exists(mark_path):
|
||||
store.remove(mark_path)
|
||||
|
||||
|
||||
def require_completion(f):
|
||||
"""This make sure that the image push correctly finished."""
|
||||
@wraps(f)
|
||||
def wrapper(namespace, repository, *args, **kwargs):
|
||||
image_id = kwargs['image_id']
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
if store.exists(store.image_mark_path(namespace, repository, image_id,
|
||||
uuid)):
|
||||
if image_is_uploading(namespace, repository, image_id, repo_image):
|
||||
abort(400, 'Image %(image_id)s is being uploaded, retry later',
|
||||
issue='upload-in-progress', image_id=kwargs['image_id'])
|
||||
|
||||
|
@ -88,17 +107,28 @@ def set_cache_headers(f):
|
|||
@set_cache_headers
|
||||
def get_image_layer(namespace, repository, image_id, headers):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
||||
profile.debug('Checking repo permissions')
|
||||
if permission.can() or model.repository_is_public(namespace, repository):
|
||||
profile.debug('Looking up repo image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
profile.debug('Looking up the layer path')
|
||||
path = store.image_layer_path(namespace, repository, image_id, uuid)
|
||||
|
||||
profile.debug('Looking up the direct download URL')
|
||||
direct_download_url = store.get_direct_download_url(path)
|
||||
|
||||
if direct_download_url:
|
||||
profile.debug('Returning direct download URL')
|
||||
return redirect(direct_download_url)
|
||||
try:
|
||||
profile.debug('Streaming layer data')
|
||||
return Response(store.stream_read(path), headers=headers)
|
||||
except IOError:
|
||||
profile.debug('Image not found')
|
||||
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||
image_id=image_id)
|
||||
|
||||
|
@ -109,25 +139,32 @@ def get_image_layer(namespace, repository, image_id, headers):
|
|||
@process_auth
|
||||
@extract_namespace_repo_from_session
|
||||
def put_image_layer(namespace, repository, image_id):
|
||||
profile.debug('Checking repo permissions')
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if not permission.can():
|
||||
abort(403)
|
||||
|
||||
profile.debug('Retrieving image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
try:
|
||||
profile.debug('Retrieving image data')
|
||||
json_data = store.get_content(store.image_json_path(namespace, repository,
|
||||
image_id, uuid))
|
||||
except IOError:
|
||||
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||
image_id=image_id)
|
||||
|
||||
profile.debug('Retrieving image path info')
|
||||
layer_path = store.image_layer_path(namespace, repository, image_id, uuid)
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id, uuid)
|
||||
|
||||
if store.exists(layer_path) and not store.exists(mark_path):
|
||||
if (store.exists(layer_path) and not
|
||||
image_is_uploading(namespace, repository, image_id, repo_image)):
|
||||
abort(409, 'Image already exists', issue='image-exists', image_id=image_id)
|
||||
|
||||
profile.debug('Storing layer data')
|
||||
|
||||
input_stream = request.stream
|
||||
if request.headers.get('transfer-encoding') == 'chunked':
|
||||
# Careful, might work only with WSGI servers supporting chunked
|
||||
|
@ -174,12 +211,12 @@ def put_image_layer(namespace, repository, image_id):
|
|||
issue='checksum-mismatch', image_id=image_id)
|
||||
|
||||
# Checksum is ok, we remove the marker
|
||||
store.remove(mark_path)
|
||||
mark_upload_complete(namespace, repository, image_id, repo_image)
|
||||
|
||||
# The layer is ready for download, send a job to the work queue to
|
||||
# process it.
|
||||
logger.debug('Queing diffs job for image: %s' % image_id)
|
||||
image_diff_queue.put(json.dumps({
|
||||
profile.debug('Adding layer to diff queue')
|
||||
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||
'namespace': namespace,
|
||||
'repository': repository,
|
||||
'image_id': image_id,
|
||||
|
@ -192,6 +229,7 @@ def put_image_layer(namespace, repository, image_id):
|
|||
@process_auth
|
||||
@extract_namespace_repo_from_session
|
||||
def put_image_checksum(namespace, repository, image_id):
|
||||
profile.debug('Checking repo permissions')
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if not permission.can():
|
||||
abort(403)
|
||||
|
@ -204,17 +242,22 @@ def put_image_checksum(namespace, repository, image_id):
|
|||
abort(400, 'Checksum not found in Cookie for image %(imaage_id)s',
|
||||
issue='missing-checksum-cookie', image_id=image_id)
|
||||
|
||||
profile.debug('Looking up repo image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
profile.debug('Looking up repo layer data')
|
||||
if not store.exists(store.image_json_path(namespace, repository, image_id,
|
||||
uuid)):
|
||||
abort(404, 'Image not found: %(image_id)s', issue='unknown-image', image_id=image_id)
|
||||
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id, uuid)
|
||||
if not store.exists(mark_path):
|
||||
profile.debug('Marking image path')
|
||||
if not image_is_uploading(namespace, repository, image_id, repo_image):
|
||||
abort(409, 'Cannot set checksum for image %(image_id)s',
|
||||
issue='image-write-error', image_id=image_id)
|
||||
|
||||
profile.debug('Storing image checksum')
|
||||
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
||||
if err:
|
||||
abort(400, err)
|
||||
|
@ -227,12 +270,12 @@ def put_image_checksum(namespace, repository, image_id):
|
|||
issue='checksum-mismatch', image_id=image_id)
|
||||
|
||||
# Checksum is ok, we remove the marker
|
||||
store.remove(mark_path)
|
||||
mark_upload_complete(namespace, repository, image_id, repo_image)
|
||||
|
||||
# The layer is ready for download, send a job to the work queue to
|
||||
# process it.
|
||||
logger.debug('Queing diffs job for image: %s' % image_id)
|
||||
image_diff_queue.put(json.dumps({
|
||||
profile.debug('Adding layer to diff queue')
|
||||
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||
'namespace': namespace,
|
||||
'repository': repository,
|
||||
'image_id': image_id,
|
||||
|
@ -247,27 +290,31 @@ def put_image_checksum(namespace, repository, image_id):
|
|||
@require_completion
|
||||
@set_cache_headers
|
||||
def get_image_json(namespace, repository, image_id, headers):
|
||||
profile.debug('Checking repo permissions')
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
if not permission.can() and not model.repository_is_public(namespace,
|
||||
repository):
|
||||
abort(403)
|
||||
|
||||
profile.debug('Looking up repo image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
profile.debug('Looking up repo layer data')
|
||||
try:
|
||||
data = store.get_content(store.image_json_path(namespace, repository,
|
||||
image_id, uuid))
|
||||
except IOError:
|
||||
flask_abort(404)
|
||||
|
||||
profile.debug('Looking up repo layer size')
|
||||
try:
|
||||
size = store.get_size(store.image_layer_path(namespace, repository,
|
||||
image_id, uuid))
|
||||
size = repo_image.image_size or repo_image.storage.image_size
|
||||
headers['X-Docker-Size'] = str(size)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
profile.debug('Retrieving checksum')
|
||||
checksum_path = store.image_checksum_path(namespace, repository, image_id,
|
||||
uuid)
|
||||
if store.exists(checksum_path):
|
||||
|
@ -284,14 +331,17 @@ def get_image_json(namespace, repository, image_id, headers):
|
|||
@require_completion
|
||||
@set_cache_headers
|
||||
def get_image_ancestry(namespace, repository, image_id, headers):
|
||||
profile.debug('Checking repo permissions')
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
if not permission.can() and not model.repository_is_public(namespace,
|
||||
repository):
|
||||
abort(403)
|
||||
|
||||
profile.debug('Looking up repo image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
profile.debug('Looking up image data')
|
||||
try:
|
||||
data = store.get_content(store.image_ancestry_path(namespace, repository,
|
||||
image_id, uuid))
|
||||
|
@ -299,8 +349,11 @@ def get_image_ancestry(namespace, repository, image_id, headers):
|
|||
abort(404, 'Image %(image_id)s not found', issue='unknown-image',
|
||||
image_id=image_id)
|
||||
|
||||
profile.debug('Converting to <-> from JSON')
|
||||
response = make_response(json.dumps(json.loads(data)), 200)
|
||||
response.headers.extend(headers)
|
||||
|
||||
profile.debug('Done')
|
||||
return response
|
||||
|
||||
|
||||
|
@ -335,10 +388,12 @@ def store_checksum(namespace, repository, image_id, uuid, checksum):
|
|||
@process_auth
|
||||
@extract_namespace_repo_from_session
|
||||
def put_image_json(namespace, repository, image_id):
|
||||
profile.debug('Checking repo permissions')
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if not permission.can():
|
||||
abort(403)
|
||||
|
||||
profile.debug('Parsing image JSON')
|
||||
try:
|
||||
data = json.loads(request.data)
|
||||
except json.JSONDecodeError:
|
||||
|
@ -351,6 +406,7 @@ def put_image_json(namespace, repository, image_id):
|
|||
abort(400, 'Missing key `id` in JSON for image: %(image_id)s',
|
||||
issue='invalid-request', image_id=image_id)
|
||||
|
||||
profile.debug('Looking up repo image')
|
||||
repo_image = model.get_repo_image(namespace, repository, image_id)
|
||||
uuid = repo_image and repo_image.storage and repo_image.storage.uuid
|
||||
|
||||
|
@ -358,12 +414,14 @@ def put_image_json(namespace, repository, image_id):
|
|||
checksum = request.headers.get('X-Docker-Checksum')
|
||||
if checksum:
|
||||
# Storing the checksum is optional at this stage
|
||||
profile.debug('Storing image checksum')
|
||||
err = store_checksum(namespace, repository, image_id, uuid, checksum)
|
||||
if err:
|
||||
abort(400, err, issue='write-error')
|
||||
|
||||
else:
|
||||
# We cleanup any old checksum in case it's a retry after a fail
|
||||
profile.debug('Cleanup old checksum')
|
||||
store.remove(store.image_checksum_path(namespace, repository, image_id,
|
||||
uuid))
|
||||
if image_id != data['id']:
|
||||
|
@ -374,19 +432,27 @@ def put_image_json(namespace, repository, image_id):
|
|||
|
||||
parent_image = None
|
||||
if parent_id:
|
||||
profile.debug('Looking up parent image')
|
||||
parent_image = model.get_repo_image(namespace, repository, parent_id)
|
||||
|
||||
parent_uuid = (parent_image and parent_image.storage and
|
||||
parent_image.storage.uuid)
|
||||
|
||||
if parent_id:
|
||||
profile.debug('Looking up parent image data')
|
||||
|
||||
if (parent_id and not
|
||||
store.exists(store.image_json_path(namespace, repository, parent_id,
|
||||
parent_uuid))):
|
||||
abort(400, 'Image %(image_id)s depends on non existing parent image %(parent_id)s',
|
||||
issue='invalid-request', image_id=image_id, parent_id=parent_id)
|
||||
|
||||
profile.debug('Looking up image storage paths')
|
||||
json_path = store.image_json_path(namespace, repository, image_id, uuid)
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id, uuid)
|
||||
if store.exists(json_path) and not store.exists(mark_path):
|
||||
|
||||
profile.debug('Checking if image already exists')
|
||||
if (store.exists(json_path) and not
|
||||
image_is_uploading(namespace, repository, image_id, repo_image)):
|
||||
abort(409, 'Image already exists', issue='image-exists', image_id=image_id)
|
||||
|
||||
# If we reach that point, it means that this is a new image or a retry
|
||||
|
@ -394,13 +460,20 @@ def put_image_json(namespace, repository, image_id):
|
|||
# save the metadata
|
||||
command_list = data.get('container_config', {}).get('Cmd', None)
|
||||
command = json.dumps(command_list) if command_list else None
|
||||
|
||||
profile.debug('Setting image metadata')
|
||||
model.set_image_metadata(image_id, namespace, repository,
|
||||
data.get('created'), data.get('comment'), command,
|
||||
parent_image)
|
||||
store.put_content(mark_path, 'true')
|
||||
|
||||
profile.debug('Putting json path')
|
||||
store.put_content(json_path, request.data)
|
||||
|
||||
profile.debug('Generating image ancestry')
|
||||
generate_ancestry(namespace, repository, image_id, uuid, parent_id,
|
||||
parent_uuid)
|
||||
|
||||
profile.debug('Done')
|
||||
return make_response('true', 200)
|
||||
|
||||
|
||||
|
|
|
@ -7,10 +7,9 @@ import base64
|
|||
from github import Github, UnknownObjectException, GithubException
|
||||
from tempfile import SpooledTemporaryFile
|
||||
|
||||
from app import app
|
||||
from app import app, userfiles as user_files
|
||||
|
||||
|
||||
user_files = app.config['USERFILES']
|
||||
client = app.config['HTTPCLIENT']
|
||||
|
||||
|
||||
|
@ -21,6 +20,10 @@ TARBALL_MIME = 'application/gzip'
|
|||
CHUNK_SIZE = 512 * 1024
|
||||
|
||||
|
||||
def should_skip_commit(message):
|
||||
return '[skip build]' in message or '[build skip]' in message
|
||||
|
||||
|
||||
class BuildArchiveException(Exception):
|
||||
pass
|
||||
|
||||
|
@ -36,6 +39,9 @@ class TriggerDeactivationException(Exception):
|
|||
class ValidationRequestException(Exception):
|
||||
pass
|
||||
|
||||
class SkipRequestException(Exception):
|
||||
pass
|
||||
|
||||
class EmptyRepositoryException(Exception):
|
||||
pass
|
||||
|
||||
|
@ -160,7 +166,7 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
try:
|
||||
hook = to_add_webhook.create_hook('web', webhook_config)
|
||||
config['hook_id'] = hook.id
|
||||
config['master_branch'] = to_add_webhook.master_branch
|
||||
config['master_branch'] = to_add_webhook.default_branch
|
||||
except GithubException:
|
||||
msg = 'Unable to create webhook on repository: %s'
|
||||
raise TriggerActivationException(msg % new_build_source)
|
||||
|
@ -219,7 +225,7 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
default_commit = repo.get_branch(repo.master_branch or 'master').commit
|
||||
default_commit = repo.get_branch(repo.default_branch or 'master').commit
|
||||
commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)
|
||||
|
||||
return [os.path.dirname(elem.path) for elem in commit_tree.tree
|
||||
|
@ -240,7 +246,7 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
gh_client = self._get_client(auth_token)
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
master_branch = repo.master_branch or 'master'
|
||||
master_branch = repo.default_branch or 'master'
|
||||
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
|
||||
except GithubException as ge:
|
||||
return None
|
||||
|
@ -292,7 +298,7 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
# compute the tag(s)
|
||||
branch = ref.split('/')[-1]
|
||||
tags = {branch}
|
||||
if branch == repo.master_branch:
|
||||
if branch == repo.default_branch:
|
||||
tags.add('latest')
|
||||
logger.debug('Pushing to tags: %s' % tags)
|
||||
|
||||
|
@ -309,13 +315,20 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
def handle_trigger_request(self, request, auth_token, config):
|
||||
payload = request.get_json()
|
||||
|
||||
if not payload:
|
||||
raise SkipRequestException()
|
||||
|
||||
if 'zen' in payload:
|
||||
raise ValidationRequestException()
|
||||
|
||||
logger.debug('Payload %s', payload)
|
||||
ref = payload['ref']
|
||||
commit_sha = payload['head_commit']['id']
|
||||
commit_message = payload['head_commit'].get('message', '')
|
||||
|
||||
if should_skip_commit(commit_message):
|
||||
raise SkipRequestException()
|
||||
|
||||
short_sha = GithubBuildTrigger.get_display_name(commit_sha)
|
||||
|
||||
gh_client = self._get_client(auth_token)
|
||||
|
@ -334,9 +347,9 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
gh_client = self._get_client(auth_token)
|
||||
repo = gh_client.get_repo(source)
|
||||
master = repo.get_branch(repo.master_branch)
|
||||
master = repo.get_branch(repo.default_branch)
|
||||
master_sha = master.commit.sha
|
||||
short_sha = GithubBuildTrigger.get_display_name(master_sha)
|
||||
ref = 'refs/heads/%s' % repo.master_branch
|
||||
ref = 'refs/heads/%s' % repo.default_branch
|
||||
|
||||
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import logging
|
||||
import stripe
|
||||
import os
|
||||
|
||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||
Blueprint)
|
||||
Blueprint, send_from_directory)
|
||||
from flask.ext.login import current_user
|
||||
from urlparse import urlparse
|
||||
|
||||
from data import model
|
||||
from data.model.oauth import DatabaseAuthorizationProvider
|
||||
from app import app
|
||||
from app import app, billing as stripe
|
||||
from auth.auth import require_session_login
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
from util.seo import render_snapshot
|
||||
from util.cache import no_cache
|
||||
from endpoints.common import common_login, render_page_template
|
||||
from endpoints.common import common_login, render_page_template, route_show_if, route_hide_if
|
||||
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||
from util.names import parse_repository_name
|
||||
from util.gravatar import compute_hash
|
||||
from auth import scopes
|
||||
|
||||
import features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
web = Blueprint('web', __name__)
|
||||
|
@ -55,6 +56,7 @@ def snapshot(path = ''):
|
|||
|
||||
@web.route('/plans/')
|
||||
@no_cache
|
||||
@route_show_if(features.BILLING)
|
||||
def plans():
|
||||
return index('')
|
||||
|
||||
|
@ -83,6 +85,12 @@ def organizations():
|
|||
def user():
|
||||
return index('')
|
||||
|
||||
@web.route('/superuser/')
|
||||
@no_cache
|
||||
@route_show_if(features.SUPER_USERS)
|
||||
def superuser():
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/signin/')
|
||||
@no_cache
|
||||
|
@ -152,7 +160,14 @@ def privacy():
|
|||
return render_page_template('privacy.html')
|
||||
|
||||
|
||||
@web.route('/robots.txt', methods=['GET'])
|
||||
@no_cache
|
||||
def robots():
|
||||
return send_from_directory('static', 'robots.txt')
|
||||
|
||||
|
||||
@web.route('/receipt', methods=['GET'])
|
||||
@route_show_if(features.BILLING)
|
||||
@require_session_login
|
||||
def receipt():
|
||||
if not current_user.is_authenticated():
|
||||
|
@ -298,7 +313,8 @@ def request_authorization_code():
|
|||
if not current_app:
|
||||
abort(404)
|
||||
|
||||
return provider._make_redirect_error_response(current_app.redirect_uri, 'redirect_uri_mismatch')
|
||||
return provider._make_redirect_error_response(current_app.redirect_uri,
|
||||
'redirect_uri_mismatch')
|
||||
|
||||
# Load the scope information.
|
||||
scope_info = scopes.get_scope_information(scope)
|
||||
|
@ -320,8 +336,9 @@ def request_authorization_code():
|
|||
|
||||
# Show the authorization page.
|
||||
return render_page_template('oauthorize.html', scopes=scope_info, application=oauth_app_view,
|
||||
enumerate=enumerate, client_id=client_id, redirect_uri=redirect_uri,
|
||||
scope=scope, csrf_token_val=generate_csrf_token())
|
||||
enumerate=enumerate, client_id=client_id,
|
||||
redirect_uri=redirect_uri, scope=scope,
|
||||
csrf_token_val=generate_csrf_token())
|
||||
|
||||
if response_type == 'token':
|
||||
return provider.get_token_response(response_type, client_id, redirect_uri, scope=scope)
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import logging
|
||||
import stripe
|
||||
import json
|
||||
|
||||
from flask import request, make_response, Blueprint
|
||||
|
||||
from app import billing as stripe
|
||||
from data import model
|
||||
from data.queue import dockerfile_build_queue
|
||||
from auth.auth import process_auth
|
||||
from auth.permissions import ModifyRepositoryPermission
|
||||
from util.invoice import renderInvoiceToHtml
|
||||
from util.email import send_invoice_email
|
||||
from util.email import send_invoice_email, send_subscription_change, send_payment_failed
|
||||
from util.names import parse_repository_name
|
||||
from util.http import abort
|
||||
from endpoints.trigger import BuildTrigger, ValidationRequestException
|
||||
from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException
|
||||
from endpoints.common import start_build
|
||||
|
||||
|
||||
|
@ -26,22 +25,44 @@ def stripe_webhook():
|
|||
request_data = request.get_json()
|
||||
logger.debug('Stripe webhook call: %s' % request_data)
|
||||
|
||||
customer_id = request_data.get('data', {}).get('object', {}).get('customer', None)
|
||||
user = model.get_user_or_org_by_customer_id(customer_id) if customer_id else None
|
||||
|
||||
event_type = request_data['type'] if 'type' in request_data else None
|
||||
if event_type == 'charge.succeeded':
|
||||
data = request_data['data'] if 'data' in request_data else {}
|
||||
obj = data['object'] if 'object' in data else {}
|
||||
invoice_id = obj['invoice'] if 'invoice' in obj else None
|
||||
customer_id = obj['customer'] if 'customer' in obj else None
|
||||
invoice_id = request_data['data']['object']['invoice']
|
||||
|
||||
if invoice_id and customer_id:
|
||||
# Find the user associated with the customer ID.
|
||||
user = model.get_user_or_org_by_customer_id(customer_id)
|
||||
if user and user.invoice_email:
|
||||
# Lookup the invoice.
|
||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||
if invoice:
|
||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||
send_invoice_email(user.email, invoice_html)
|
||||
if user and user.invoice_email:
|
||||
# Lookup the invoice.
|
||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||
if invoice:
|
||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||
send_invoice_email(user.email, invoice_html)
|
||||
|
||||
elif event_type.startswith('customer.subscription.'):
|
||||
cust_email = user.email if user is not None else 'unknown@domain.com'
|
||||
quay_username = user.username if user is not None else 'unknown'
|
||||
|
||||
change_type = ''
|
||||
if event_type.endswith('.deleted'):
|
||||
plan_id = request_data['data']['object']['plan']['id']
|
||||
change_type = 'canceled %s' % plan_id
|
||||
send_subscription_change(change_type, customer_id, cust_email, quay_username)
|
||||
elif event_type.endswith('.created'):
|
||||
plan_id = request_data['data']['object']['plan']['id']
|
||||
change_type = 'subscribed %s' % plan_id
|
||||
send_subscription_change(change_type, customer_id, cust_email, quay_username)
|
||||
elif event_type.endswith('.updated'):
|
||||
if 'previous_attributes' in request_data['data']:
|
||||
if 'plan' in request_data['data']['previous_attributes']:
|
||||
old_plan = request_data['data']['previous_attributes']['plan']['id']
|
||||
new_plan = request_data['data']['object']['plan']['id']
|
||||
change_type = 'switched %s -> %s' % (old_plan, new_plan)
|
||||
send_subscription_change(change_type, customer_id, cust_email, quay_username)
|
||||
|
||||
elif event_type == 'invoice.payment_failed':
|
||||
if user:
|
||||
send_payment_failed(user.email, user.username)
|
||||
|
||||
return make_response('Okay')
|
||||
|
||||
|
@ -73,6 +94,10 @@ def build_trigger_webhook(namespace, repository, trigger_uuid):
|
|||
# This was just a validation request, we don't need to build anything
|
||||
return make_response('Okay')
|
||||
|
||||
except SkipRequestException:
|
||||
# The build was requested to be skipped
|
||||
return make_response('Okay')
|
||||
|
||||
pull_robot_name = model.get_pull_robot_name(trigger)
|
||||
repo = model.get_repository(namespace, repository)
|
||||
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
|
||||
|
|
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 import model
|
||||
from data.model import oauth
|
||||
from app import app
|
||||
from app import app, storage as store
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
store = app.config['STORAGE']
|
||||
|
||||
SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i
|
||||
for i in range(1, 10)]
|
||||
|
@ -149,8 +148,7 @@ def setup_database_for_testing(testcase):
|
|||
|
||||
# Sanity check to make sure we're not killing our prod db
|
||||
db = model.db
|
||||
if (not isinstance(model.db, SqliteDatabase) or
|
||||
app.config['DB_DRIVER'] is not SqliteDatabase):
|
||||
if not isinstance(model.db, SqliteDatabase):
|
||||
raise RuntimeError('Attempted to wipe production database!')
|
||||
|
||||
global db_initialized_for_testing
|
||||
|
@ -198,6 +196,8 @@ def initialize_database():
|
|||
LogEntryKind.create(name='push_repo')
|
||||
LogEntryKind.create(name='pull_repo')
|
||||
LogEntryKind.create(name='delete_repo')
|
||||
LogEntryKind.create(name='create_tag')
|
||||
LogEntryKind.create(name='move_tag')
|
||||
LogEntryKind.create(name='delete_tag')
|
||||
LogEntryKind.create(name='add_repo_permission')
|
||||
LogEntryKind.create(name='change_repo_permission')
|
||||
|
@ -241,8 +241,7 @@ def wipe_database():
|
|||
|
||||
# Sanity check to make sure we're not killing our prod db
|
||||
db = model.db
|
||||
if (not isinstance(model.db, SqliteDatabase) or
|
||||
app.config['DB_DRIVER'] is not SqliteDatabase):
|
||||
if not isinstance(model.db, SqliteDatabase):
|
||||
raise RuntimeError('Attempted to wipe production database!')
|
||||
|
||||
drop_model_tables(all_models, fail_silently=True)
|
||||
|
@ -490,7 +489,8 @@ def populate_database():
|
|||
'service': trigger.service.name})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.config['LOGGING_CONFIG']()
|
||||
log_level = getattr(logging, app.config['LOGGING_LEVEL'])
|
||||
logging.basicConfig(level=log_level)
|
||||
initialize_database()
|
||||
|
||||
if app.config.get('POPULATE_DB_TEST_DATA', False):
|
||||
|
|
|
@ -18,7 +18,6 @@ python-daemon
|
|||
paramiko
|
||||
python-digitalocean
|
||||
xhtml2pdf
|
||||
logstash_formatter
|
||||
redis
|
||||
hiredis
|
||||
git+https://github.com/DevTable/docker-py.git
|
||||
|
@ -26,4 +25,10 @@ loremipsum
|
|||
pygithub
|
||||
flask-restful
|
||||
jsonschema
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
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-RESTful==0.2.12
|
||||
Jinja2==2.7.2
|
||||
MarkupSafe==0.19
|
||||
Pillow==2.3.1
|
||||
Mako==0.9.1
|
||||
MarkupSafe==0.21
|
||||
Pillow==2.4.0
|
||||
PyGithub==1.24.1
|
||||
PyMySQL==0.6.1
|
||||
PyMySQL==0.6.2
|
||||
PyPDF2==1.21
|
||||
SQLAlchemy==0.9.4
|
||||
Werkzeug==0.9.4
|
||||
alembic==0.6.4
|
||||
aniso8601==0.82
|
||||
argparse==1.2.1
|
||||
beautifulsoup4==4.3.2
|
||||
blinker==1.3
|
||||
boto==2.27.0
|
||||
distribute==0.6.34
|
||||
git+https://github.com/DevTable/docker-py.git
|
||||
ecdsa==0.11
|
||||
gevent==1.0
|
||||
gevent==1.0.1
|
||||
greenlet==0.4.2
|
||||
gunicorn==18.0
|
||||
hiredis==0.1.2
|
||||
html5lib==1.0b3
|
||||
itsdangerous==0.23
|
||||
hiredis==0.1.3
|
||||
html5lib==0.999
|
||||
itsdangerous==0.24
|
||||
jsonschema==2.3.0
|
||||
lockfile==0.9.1
|
||||
logstash-formatter==0.5.8
|
||||
loremipsum==1.0.2
|
||||
marisa-trie==0.6
|
||||
mixpanel-py==3.1.2
|
||||
mock==1.0.1
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
paramiko==1.13.0
|
||||
peewee==2.2.2
|
||||
peewee==2.2.3
|
||||
py-bcrypt==0.4
|
||||
pyPdf==1.13
|
||||
pycrypto==2.6.1
|
||||
python-daemon==1.6
|
||||
python-dateutil==2.2
|
||||
python-digitalocean==0.7
|
||||
python-magic==0.4.6
|
||||
pytz==2014.2
|
||||
raven==4.2.1
|
||||
redis==2.9.1
|
||||
reportlab==2.7
|
||||
requests==2.2.1
|
||||
six==1.6.1
|
||||
stripe==1.12.2
|
||||
stripe==1.14.0
|
||||
websocket-client==0.11.0
|
||||
wsgiref==0.1.2
|
||||
xhtml2pdf==0.0.5
|
||||
xhtml2pdf==0.0.6
|
||||
|
|
|
@ -676,6 +676,10 @@ i.toggle-icon:hover {
|
|||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.phase-icon.pulling {
|
||||
background-color: #cab442;
|
||||
}
|
||||
|
||||
.phase-icon.building {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
@ -995,6 +999,24 @@ i.toggle-icon:hover {
|
|||
}
|
||||
}
|
||||
|
||||
.visible-xl {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visible-xl-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.visible-xl {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.visible-xl-inline {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.plans-list .plan-box .description {
|
||||
color: white;
|
||||
margin-top: 6px;
|
||||
|
@ -1528,22 +1550,22 @@ p.editable:hover i {
|
|||
border: 0px;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings {
|
||||
.tag-specific-images-view .image-listings {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing {
|
||||
.tag-specific-images-view .image-listings .image-listing {
|
||||
margin: 4px;
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-id {
|
||||
.tag-specific-images-view .image-listings .image-listing .image-listing-id {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-line {
|
||||
.tag-specific-images-view .image-listings .image-listing .image-listing-line {
|
||||
border-left: 2px solid steelblue;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
@ -1554,15 +1576,15 @@ p.editable:hover i {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-line {
|
||||
.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-line {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing.child .image-listing-line {
|
||||
.tag-specific-images-view .image-listings .image-listing.child .image-listing-line {
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing .image-listing-circle {
|
||||
.tag-specific-images-view .image-listings .image-listing .image-listing-circle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
|
||||
|
@ -1575,14 +1597,55 @@ p.editable:hover i {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-circle {
|
||||
.tag-specific-images-view .image-listings .image-listing.tag-image .image-listing-circle {
|
||||
background: steelblue;
|
||||
}
|
||||
|
||||
#confirmdeleteTagModal .more-changes {
|
||||
.tag-specific-images-view .more-changes {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.repo.container-fluid {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.repo.container-fluid {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.repo.container-fluid {
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.repo.container-fluid .col-md-4 {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.repo.container-fluid .col-md-8 {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.repo .current-context {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.repo .current-context-icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.repo .header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
|
@ -1644,6 +1707,10 @@ p.editable:hover i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo .repo-controls .dropdown {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo .repo-controls .count {
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
|
@ -1798,6 +1865,77 @@ p.editable:hover i {
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.repo .image-comment {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.repo .image-section {
|
||||
margin-top: 12px;
|
||||
padding-bottom: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo .image-section .tag {
|
||||
margin: 2px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
||||
.repo .image-section .section-icon {
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
margin-left: -4px;
|
||||
margin-right: 14px;
|
||||
color: #bbb;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.repo .image-section .section-info {
|
||||
padding: 4px;
|
||||
padding-left: 6px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
|
||||
background-color: #f5f5f5;
|
||||
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.repo .image-section .section-info-with-dropdown {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.repo .image-section .dropdown {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 2px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.repo .image-section .dropdown-button {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
background: white;
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
border: 1px solid #eee;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-list {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
@ -2106,19 +2244,11 @@ p.editable:hover i {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
.repo .small-changes-container:before {
|
||||
content: "File Changes: ";
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.repo .formatted-command {
|
||||
margin-top: 4px;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
|
||||
.repo .formatted-command.trimmed {
|
||||
|
@ -2127,16 +2257,22 @@ p.editable:hover i {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.repo .changes-count-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.repo .change-count {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo .change-count b {
|
||||
font-weight: normal;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.repo .changes-container .well {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.repo .changes-container i.fa-plus-square {
|
||||
color: rgb(73, 209, 73);
|
||||
}
|
||||
|
@ -2154,7 +2290,7 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo .change-count i {
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -2166,6 +2302,7 @@ p.editable:hover i {
|
|||
|
||||
.repo .more-changes {
|
||||
padding: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.repo #collapseChanges .well {
|
||||
|
@ -2316,11 +2453,6 @@ p.editable:hover i {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-admin .form-change input {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-admin .convert-form h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
@ -2411,10 +2543,13 @@ p.editable:hover i {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.tags .tag, #confirmdeleteTagModal .tag {
|
||||
.tags .tag, .tag-specific-images-view .tag {
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tooltip-tags {
|
||||
|
@ -2464,42 +2599,42 @@ p.editable:hover i {
|
|||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
#repository-usage-chart {
|
||||
.usage-chart {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#repository-usage-chart .count-text {
|
||||
.usage-chart .count-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-at path.arc-0 {
|
||||
.usage-chart.limit-at path.arc-0 {
|
||||
fill: #c09853;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-over path.arc-0 {
|
||||
.usage-chart.limit-over path.arc-0 {
|
||||
fill: #b94a48;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-near path.arc-0 {
|
||||
.usage-chart.limit-near path.arc-0 {
|
||||
fill: #468847;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-over path.arc-1 {
|
||||
.usage-chart.limit-over path.arc-1 {
|
||||
fill: #fcf8e3;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-at path.arc-1 {
|
||||
.usage-chart.limit-at path.arc-1 {
|
||||
fill: #f2dede;
|
||||
}
|
||||
|
||||
#repository-usage-chart.limit-near path.arc-1 {
|
||||
.usage-chart.limit-near path.arc-1 {
|
||||
fill: #dff0d8;
|
||||
}
|
||||
|
||||
.plan-manager-element .usage-caption {
|
||||
.usage-caption {
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
font-size: 26px;
|
||||
|
@ -3630,4 +3765,17 @@ pre.command:before {
|
|||
|
||||
.trigger-option-section table td {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.user-row.super-user td {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
|
||||
.user-row .user-class {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.form-change input {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</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="/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>
|
||||
</ul>
|
||||
|
||||
|
@ -65,6 +65,7 @@
|
|||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
@ -17,10 +17,15 @@
|
|||
You are nearing the number of allowed private repositories. It might be time to think about
|
||||
upgrading your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
|
||||
</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 -->
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -57,7 +62,8 @@
|
|||
ng-click="changeSubscription(plan.stripeId)">
|
||||
<span class="quay-spinner" ng-show="planChanging"></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 class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
||||
ng-click="cancelSubscription()">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
<span ng-transclude></span>
|
||||
</button>
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Setup new build trigger</h4>
|
||||
</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 -->
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||
<div ng-switch-when="github">
|
||||
|
@ -34,7 +37,7 @@
|
|||
The
|
||||
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
|
||||
<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">
|
||||
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
||||
</a> which requires
|
||||
|
@ -45,7 +48,7 @@
|
|||
<table style="width: 100%;" ng-show="pullRequirements">
|
||||
<tr>
|
||||
<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:
|
||||
</div>
|
||||
</td>
|
||||
|
@ -76,10 +79,10 @@
|
|||
filter="['robot']"></div>
|
||||
|
||||
<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 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>.
|
||||
</div>
|
||||
</td>
|
||||
|
@ -90,8 +93,8 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
|
||||
ng-click="activate">Finished</button>
|
||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements || activating"
|
||||
ng-click="activate()">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
placeholder="Password" ng-model="user.password">
|
||||
<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>
|
||||
<span class="inner-text">OR</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
|
||||
</a>
|
||||
</form>
|
||||
|
|
|
@ -10,14 +10,19 @@
|
|||
<div class="form-group signin-buttons">
|
||||
<button id="signupButton"
|
||||
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>
|
||||
<span class="social-alternate">
|
||||
analytics-on analytics-event="register">
|
||||
<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>
|
||||
<span class="inner-text">OR</span>
|
||||
</span>
|
||||
<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>
|
||||
<p class="help-block">No credit card required.</p>
|
||||
class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']">
|
||||
<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>
|
||||
</form>
|
||||
<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.
|
||||
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
|
||||
|
||||
$scope.tour = {
|
||||
'title': 'Quay.io Tutorial',
|
||||
'initialScope': {
|
||||
'showSudo': showSudo
|
||||
'showSudo': showSudo,
|
||||
'domainName': Config.getDomain()
|
||||
},
|
||||
'steps': [
|
||||
{
|
||||
|
@ -262,7 +263,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
|
|||
loadPublicRepos();
|
||||
}
|
||||
|
||||
function LandingCtrl($scope, UserService, ApiService) {
|
||||
function LandingCtrl($scope, UserService, ApiService, Features, Config) {
|
||||
$scope.namespace = null;
|
||||
|
||||
$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 name = $routeParams.name;
|
||||
|
||||
|
@ -349,6 +362,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
getBuildInfo($scope.repo);
|
||||
startBuildInfoTimer($scope.repo);
|
||||
};
|
||||
|
||||
|
@ -384,9 +398,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
$scope.getMoreCount = function(changes) {
|
||||
if (!changes) { return 0; }
|
||||
var addedDisplayed = Math.min(5, changes.added.length);
|
||||
var removedDisplayed = Math.min(5, changes.removed.length);
|
||||
var changedDisplayed = Math.min(5, changes.changed.length);
|
||||
var addedDisplayed = Math.min(2, changes.added.length);
|
||||
var removedDisplayed = Math.min(2, changes.removed.length);
|
||||
var changedDisplayed = Math.min(2, changes.changed.length);
|
||||
|
||||
return (changes.added.length + changes.removed.length + changes.changed.length) -
|
||||
addedDisplayed - removedDisplayed - changedDisplayed;
|
||||
|
@ -415,57 +429,21 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
$location.search('tag', null);
|
||||
$location.search('image', imageId.substr(0, 12));
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAddTag = function(image) {
|
||||
$scope.toTagImage = image;
|
||||
$('#addTagModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.tagSpecificImages = function(tagName) {
|
||||
if (!tagName) { return []; }
|
||||
$scope.isOwnedTag = function(image, tagName) {
|
||||
if (!image || !tagName) { return false; }
|
||||
return image.tags.indexOf(tagName) >= 0;
|
||||
};
|
||||
|
||||
var tag = $scope.repo.tags[tagName];
|
||||
if (!tag) { return []; }
|
||||
|
||||
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.isAnotherImageTag = function(image, tagName) {
|
||||
if (!image || !tagName) { return false; }
|
||||
return image.tags.indexOf(tagName) < 0 && $scope.repo.tags[tagName];
|
||||
};
|
||||
|
||||
$scope.askDeleteTag = function(tagName) {
|
||||
|
@ -475,6 +453,39 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
$('#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) {
|
||||
if (!$scope.repo.can_admin) { return; }
|
||||
$('#confirmdeleteTagModal').modal('hide');
|
||||
|
@ -555,20 +566,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
$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) {
|
||||
if (!repo) { return 0; }
|
||||
var count = 0;
|
||||
|
@ -734,33 +731,34 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
$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
|
||||
if ($scope.currentTag) {
|
||||
$scope.tree.setTag($scope.currentTag.name);
|
||||
// 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(); });
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$scope.setImage($routeParams.image);
|
||||
}
|
||||
|
@ -844,7 +842,7 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
|
|||
if (dockerfile && dockerfile.canRead) {
|
||||
DataFileService.blobToString(dockerfile.toBlob(), function(result) {
|
||||
$scope.$apply(function() {
|
||||
$scope.dockerFilePath = dockerfilePath;
|
||||
$scope.dockerFilePath = dockerfilePath || 'Dockerfile';
|
||||
$scope.dockerFileContents = result;
|
||||
});
|
||||
});
|
||||
|
@ -854,8 +852,11 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou
|
|||
};
|
||||
|
||||
var notarchive = function() {
|
||||
$scope.dockerFileContents = DataFileService.arrayToString(uint8array);
|
||||
$scope.loaded = true;
|
||||
DataFileService.arrayToString(uint8array, function(r) {
|
||||
$scope.dockerFilePath = 'Dockerfile';
|
||||
$scope.dockerFileContents = r;
|
||||
$scope.loaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
// root build object to remain the same object.
|
||||
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
|
||||
var currentBuild = $scope.builds[$scope.currentBuildIndex];
|
||||
checkPollTimer();
|
||||
|
||||
// Load the updated logs for the build.
|
||||
|
@ -1110,6 +1112,18 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
processLogs(resp['logs'], resp['start']);
|
||||
$scope.logStartIndex = resp['total'];
|
||||
$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() {
|
||||
$scope.polling = false;
|
||||
});
|
||||
|
@ -1152,7 +1166,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
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 name = $routeParams.name;
|
||||
|
||||
|
@ -1170,12 +1184,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
$scope.getBadgeFormat = function(format, repo) {
|
||||
if (!repo) { return; }
|
||||
|
||||
var imageUrl = 'https://quay.io/repository/' + namespace + '/' + name + '/status';
|
||||
var imageUrl = Config.getUrl('/' + namespace + '/' + name + '/status');
|
||||
if (!$scope.repo.is_public) {
|
||||
imageUrl += '?token=' + $scope.repo.status_token;
|
||||
}
|
||||
|
||||
var linkUrl = 'https://quay.io/repository/' + namespace + '/' + name;
|
||||
var linkUrl = Config.getUrl('/' + namespace + '/' + name);
|
||||
|
||||
switch (format) {
|
||||
case 'svg':
|
||||
|
@ -1492,6 +1506,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$scope.deleteTrigger = function(trigger) {
|
||||
if (!trigger) { return; }
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'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,
|
||||
$routeParams, $http, UIService) {
|
||||
$routeParams, $http, UIService, Features) {
|
||||
$scope.Features = Features;
|
||||
|
||||
if ($routeParams['migrate']) {
|
||||
$('#migrateTab').tab('show')
|
||||
}
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
if (!Features.GITHUB_LOGIN) { return; }
|
||||
|
||||
$scope.cuser = jQuery.extend({}, user);
|
||||
|
||||
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.org = {};
|
||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
$scope.githubClientId = KeyService.githubLoginClientId;
|
||||
$scope.authorizedApps = null;
|
||||
|
||||
$scope.logsShown = 0;
|
||||
|
@ -1640,13 +1660,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
};
|
||||
|
||||
$scope.showConvertForm = function() {
|
||||
PlanService.getMatchingBusinessPlan(function(plan) {
|
||||
$scope.org.plan = plan;
|
||||
});
|
||||
if (Features.BILLING) {
|
||||
PlanService.getMatchingBusinessPlan(function(plan) {
|
||||
$scope.org.plan = plan;
|
||||
});
|
||||
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.orgPlans = plans;
|
||||
});
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.orgPlans = plans;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.convertStep = 1;
|
||||
};
|
||||
|
@ -1661,7 +1683,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
var data = {
|
||||
'adminUser': $scope.org.adminUser,
|
||||
'adminPassword': $scope.org.adminPassword,
|
||||
'plan': $scope.org.plan.stripeId
|
||||
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
|
||||
};
|
||||
|
||||
ApiService.convertUserToOrganization(data).then(function(resp) {
|
||||
|
@ -1856,7 +1878,7 @@ function V1Ctrl($scope, $location, UserService) {
|
|||
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);
|
||||
|
||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
|
@ -1978,13 +2000,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
var checkPrivateAllowed = function() {
|
||||
if (!$scope.repo || !$scope.repo.namespace) { return; }
|
||||
|
||||
if (!Features.BILLING) {
|
||||
$scope.checkingPlan = false;
|
||||
$scope.planRequired = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.checkingPlan = true;
|
||||
|
||||
var isUserNamespace = $scope.isUserNamespace;
|
||||
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
|
||||
$scope.checkingPlan = false;
|
||||
|
||||
if (resp['privateAllowed']) {
|
||||
if (resp['privateAllowed']) {
|
||||
$scope.planRequired = null;
|
||||
return;
|
||||
}
|
||||
|
@ -2104,18 +2132,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
|||
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;
|
||||
|
||||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans;
|
||||
$scope.plan_map = {};
|
||||
|
||||
for (var i = 0; i < plans.length; ++i) {
|
||||
$scope.plan_map[plans[i].stripeId] = plans[i];
|
||||
}
|
||||
});
|
||||
if (Features.BILLING) {
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans;
|
||||
$scope.plan_map = {};
|
||||
|
||||
for (var i = 0; i < plans.length; ++i) {
|
||||
$scope.plan_map[plans[i].stripeId] = plans[i];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.orgname = orgname;
|
||||
$scope.membersLoading = true;
|
||||
|
@ -2297,34 +2327,43 @@ function OrgsCtrl($scope, UserService) {
|
|||
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);
|
||||
|
||||
var requested = $routeParams['plan'];
|
||||
|
||||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans;
|
||||
$scope.currentPlan = null;
|
||||
if (requested) {
|
||||
PlanService.getPlan(requested, function(plan) {
|
||||
$scope.currentPlan = plan;
|
||||
});
|
||||
}
|
||||
});
|
||||
if (Features.BILLING) {
|
||||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans;
|
||||
$scope.holder.currentPlan = null;
|
||||
if (requested) {
|
||||
PlanService.getPlan(requested, function(plan) {
|
||||
$scope.holder.currentPlan = plan;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.signedIn = function() {
|
||||
PlanService.handleNotedPlan();
|
||||
if (Features.BILLING) {
|
||||
PlanService.handleNotedPlan();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.signinStarted = function() {
|
||||
PlanService.getMinimumPlan(1, true, function(plan) {
|
||||
PlanService.notePlan(plan.stripeId);
|
||||
});
|
||||
if (Features.BILLING) {
|
||||
PlanService.getMinimumPlan(1, true, function(plan) {
|
||||
PlanService.notePlan(plan.stripeId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setPlan = function(plan) {
|
||||
$scope.currentPlan = plan;
|
||||
$scope.holder.currentPlan = plan;
|
||||
};
|
||||
|
||||
$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 ($scope.currentPlan.price == 0) {
|
||||
if (!Features.BILLING || $scope.holder.currentPlan.price == 0) {
|
||||
showOrg();
|
||||
return;
|
||||
}
|
||||
|
@ -2366,7 +2405,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
|||
'failure': showOrg
|
||||
};
|
||||
|
||||
PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, callbacks);
|
||||
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
||||
}, function(result) {
|
||||
$scope.creating = false;
|
||||
$scope.createError = result.data.message || result.data;
|
||||
|
@ -2545,4 +2584,135 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
// Load the organization and application info.
|
||||
loadOrganization();
|
||||
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({
|
||||
'type': 'hideTagMenu'
|
||||
});
|
||||
|
||||
$(that).trigger({
|
||||
'type': 'hideImageMenu'
|
||||
});
|
||||
});
|
||||
|
||||
overscroll.on('scroll', function() {
|
||||
$(that).trigger({
|
||||
'type': 'hideTagMenu'
|
||||
});
|
||||
|
||||
$(that).trigger({
|
||||
'type': 'hideImageMenu'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -178,6 +186,11 @@ ImageHistoryTree.prototype.draw = function(container) {
|
|||
// Save the container.
|
||||
this.container_ = container;
|
||||
|
||||
if (!$('#' + container)[0]) {
|
||||
this.container_ = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the tree and all its components.
|
||||
var tree = d3.layout.tree()
|
||||
.separation(function() { return 2; });
|
||||
|
@ -185,11 +198,10 @@ ImageHistoryTree.prototype.draw = function(container) {
|
|||
var diagonal = d3.svg.diagonal()
|
||||
.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");
|
||||
|
||||
var vis = rootSvg.append("svg:g");
|
||||
|
||||
var formatComment = this.formatComment_;
|
||||
var formatTime = this.formatTime_;
|
||||
var formatCommand = this.formatCommand_;
|
||||
|
@ -254,6 +266,8 @@ ImageHistoryTree.prototype.draw = function(container) {
|
|||
|
||||
this.setTag_(this.currentTag_);
|
||||
this.setupOverscroll_();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
@ -664,7 +678,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
if (d.collapsed) { that.expandCollapsed_(d); }
|
||||
})
|
||||
.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")
|
||||
.append("svg:text")
|
||||
|
@ -732,15 +758,16 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
return '';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
var html = '<div style="width: ' + DEPTH_HEIGHT + 'px">';
|
||||
for (var i = 0; i < d.tags.length; ++i) {
|
||||
var tag = d.tags[i];
|
||||
var kind = 'default';
|
||||
if (tag == currentTag) {
|
||||
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;
|
||||
});
|
||||
|
||||
|
@ -909,6 +936,8 @@ FileTreeBase.prototype.calculateDimensions_ = function(container) {
|
|||
* Updates the dimensions of the tree.
|
||||
*/
|
||||
FileTreeBase.prototype.updateDimensions_ = function() {
|
||||
if (!this.rootSvg_) { return; }
|
||||
|
||||
var container = this.container_;
|
||||
var dimensions = this.calculateDimensions_(container);
|
||||
|
||||
|
@ -1106,7 +1135,12 @@ FileTreeBase.prototype.update_ = function(source) {
|
|||
};
|
||||
|
||||
// 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_());
|
||||
|
||||
// Compute the flattened node list.
|
||||
|
@ -1359,7 +1393,7 @@ FileTree.prototype.getNodesHeight = function() {
|
|||
/**
|
||||
* Based off of http://bl.ocks.org/mbostock/1346410
|
||||
*/
|
||||
function RepositoryUsageChart() {
|
||||
function UsageChart() {
|
||||
this.total_ = null;
|
||||
this.count_ = null;
|
||||
this.drawn_ = false;
|
||||
|
@ -1369,7 +1403,7 @@ function RepositoryUsageChart() {
|
|||
/**
|
||||
* 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; }
|
||||
this.total_ = total;
|
||||
this.count_ = count;
|
||||
|
@ -1380,7 +1414,7 @@ RepositoryUsageChart.prototype.update = function(count, total) {
|
|||
/**
|
||||
* 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 (this.total_ === null) { return; }
|
||||
|
||||
|
@ -1439,7 +1473,7 @@ RepositoryUsageChart.prototype.drawInternal_ = function() {
|
|||
/**
|
||||
* Draws the chart in the given container.
|
||||
*/
|
||||
RepositoryUsageChart.prototype.draw = function(container) {
|
||||
UsageChart.prototype.draw = function(container) {
|
||||
var cw = 200;
|
||||
var ch = 200;
|
||||
var radius = Math.min(cw, ch) / 2;
|
||||
|
@ -1668,7 +1702,12 @@ LogUsageChart.prototype.handleStateChange_ = function(e) {
|
|||
*/
|
||||
LogUsageChart.prototype.draw = function(container, logData, startDate, endDate) {
|
||||
// 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.
|
||||
var offsetDate = function(d, days) {
|
||||
|
@ -1716,7 +1755,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate)
|
|||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windoweResize(chart.update);
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
||||
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
||||
|
|
|
@ -135,7 +135,7 @@ angular.module("angular-tour", [])
|
|||
};
|
||||
|
||||
var fireMixpanelEvent = function() {
|
||||
if (!$scope.step || !mixpanel) { return; }
|
||||
if (!$scope.step || !window['mixpanel']) { return; }
|
||||
|
||||
var eventName = $scope.step['mixpanelEvent'];
|
||||
if (eventName) {
|
||||
|
|
|
@ -230,3 +230,4 @@ var saveAs = saveAs
|
|||
// with an attribute `content` that corresponds to the window
|
||||
|
||||
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;
|
||||
else
|
||||
Markdown = {};
|
||||
|
||||
window.Markdown = Markdown;
|
||||
|
||||
// The following text is included for historical reasons, but should
|
||||
// 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>
|
||||
<span data-title="{{change.file}}">
|
||||
<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>
|
||||
</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