Merge remote-tracking branch 'origin/master' into tagyourit
Conflicts: static/css/quay.css static/js/graphing.js static/partials/view-repo.html test/data/test.db
This commit is contained in:
commit
3f42d15335
132 changed files with 4266 additions and 1924 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
venv
|
||||
static/snapshots/
|
||||
screenshots/screenshots/
|
||||
stack
|
||||
|
|
49
Dockerfile
Normal file
49
Dockerfile
Normal file
|
@ -0,0 +1,49 @@
|
|||
FROM phusion/baseimage:0.9.9
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV HOME /root
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core g++ libmagic1
|
||||
|
||||
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 screenshots screenshots
|
||||
ADD static static
|
||||
ADD storage storage
|
||||
ADD templates templates
|
||||
ADD test test
|
||||
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 TEST=true venv/bin/python -m unittest discover
|
||||
|
||||
VOLUME ["/conf/stack", "/mnt/logs"]
|
||||
|
||||
EXPOSE 443 80
|
||||
|
||||
CMD ["/sbin/my_init"]
|
77
README.md
77
README.md
|
@ -1,64 +1,49 @@
|
|||
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
|
||||
```
|
||||
|
||||
start the quay processes:
|
||||
|
||||
```
|
||||
git clone https://bitbucket.org/yackob03/quayconfig.git
|
||||
sudo docker pull quay.io/quay/quay
|
||||
sudo mkdir -p /mnt/logs/
|
||||
sudo docker run -d -p 80:80 -p 443:443 -v /mnt/logs:/mnt/logs -v `pwd`/quayconfig/production:/conf/stack quay.io/quay/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
|
56
app.py
56
app.py
|
@ -1,48 +1,46 @@
|
|||
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 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)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import logging
|
||||
|
||||
from app import app as application
|
||||
from data.model import db as model_db
|
||||
|
||||
# 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)
|
||||
|
||||
|
|
|
@ -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.
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,7 +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'
|
||||
|
|
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'
|
22
conf/nginx-enterprise-nossl.conf
Normal file
22
conf/nginx-enterprise-nossl.conf
Normal file
|
@ -0,0 +1,22 @@
|
|||
include root-base.conf;
|
||||
|
||||
worker_processes 2;
|
||||
|
||||
user root nogroup;
|
||||
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
server {
|
||||
include server-base.conf;
|
||||
|
||||
listen 80 default;
|
||||
|
||||
location /static/ {
|
||||
# checks for static file, if not found proxy to app
|
||||
alias /static/;
|
||||
}
|
||||
}
|
||||
}
|
32
conf/nginx-enterprise.conf
Normal file
32
conf/nginx-enterprise.conf
Normal file
|
@ -0,0 +1,32 @@
|
|||
include root-base.conf;
|
||||
|
||||
worker_processes 2;
|
||||
|
||||
user root nogroup;
|
||||
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
include hosted-http-base.conf;
|
||||
|
||||
server {
|
||||
include server-base.conf;
|
||||
|
||||
listen 443 default;
|
||||
|
||||
ssl on;
|
||||
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;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location /static/ {
|
||||
# checks for static file, if not found proxy to app
|
||||
alias /static/;
|
||||
}
|
||||
}
|
||||
}
|
341
config.py
341
config.py
|
@ -3,183 +3,17 @@ 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 build_requests_session():
|
||||
sess = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(pool_connections=100,
|
||||
pool_maxsize=100)
|
||||
sess.mount('http://', adapter)
|
||||
sess.mount('https://', adapter)
|
||||
return sess
|
||||
|
||||
|
||||
def logs_init_builder(level=logging.DEBUG,
|
||||
|
@ -194,80 +28,119 @@ def logs_init_builder(level=logging.DEBUG,
|
|||
|
||||
return init_logs
|
||||
|
||||
|
||||
def build_requests_session():
|
||||
sess = requests.Session()
|
||||
adapter = requests.adapters.HTTPAdapter(pool_connections=100,
|
||||
pool_maxsize=100)
|
||||
sess.mount('http://', adapter)
|
||||
sess.mount('https://', adapter)
|
||||
return sess
|
||||
# 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']
|
||||
|
||||
|
||||
class LargePoolHttpClient(object):
|
||||
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_CONFIG = logs_init_builder(formatter=logging.Formatter())
|
||||
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"
|
||||
|
||||
# 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 = True
|
||||
|
||||
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
|
||||
|
|
232
data/billing.py
Normal file
232
data/billing.py
Normal file
|
@ -0,0 +1,232 @@
|
|||
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()),
|
||||
})
|
||||
|
||||
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():
|
||||
|
@ -176,6 +202,7 @@ class RepositoryBuildTrigger(BaseModel):
|
|||
auth_token = CharField()
|
||||
config = TextField(default='{}')
|
||||
write_token = ForeignKeyField(AccessToken, null=True)
|
||||
pull_robot = ForeignKeyField(User, null=True, related_name='triggerpullrobot')
|
||||
|
||||
|
||||
class EmailConfirmation(BaseModel):
|
||||
|
@ -244,10 +271,11 @@ class RepositoryBuild(BaseModel):
|
|||
started = DateTimeField(default=datetime.now)
|
||||
display_name = CharField()
|
||||
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
|
||||
pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot')
|
||||
|
||||
|
||||
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):
|
||||
|
@ -61,8 +61,10 @@ class InvalidBuildTriggerException(DataModelException):
|
|||
def create_user(username, password, email, is_organization=False):
|
||||
if not validate_email(email):
|
||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||
if not validate_username(username):
|
||||
raise InvalidUsernameException('Invalid username: %s' % username)
|
||||
|
||||
(username_valid, username_issue) = validate_username(username)
|
||||
if not username_valid:
|
||||
raise InvalidUsernameException('Invalid username %s: %s' % (username, username_issue))
|
||||
|
||||
# We allow password none for the federated login case.
|
||||
if password is not None and not validate_password(password):
|
||||
|
@ -125,9 +127,10 @@ def create_organization(name, email, creating_user):
|
|||
|
||||
|
||||
def create_robot(robot_shortname, parent):
|
||||
if not validate_username(robot_shortname):
|
||||
raise InvalidRobotException('The name for the robot \'%s\' is invalid.' %
|
||||
robot_shortname)
|
||||
(username_valid, username_issue) = validate_username(robot_shortname)
|
||||
if not username_valid:
|
||||
raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' %
|
||||
(robot_shortname, username_issue))
|
||||
|
||||
username = format_robot_username(parent.username, robot_shortname)
|
||||
|
||||
|
@ -154,6 +157,16 @@ def create_robot(robot_shortname, parent):
|
|||
raise DataModelException(ex.message)
|
||||
|
||||
|
||||
def lookup_robot(robot_username):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
found = list(joined.where(LoginService.name == 'quayrobot',
|
||||
User.username == robot_username))
|
||||
if not found or len(found) < 1 or not found[0].robot:
|
||||
return None
|
||||
|
||||
return found[0]
|
||||
|
||||
|
||||
def verify_robot(robot_username, password):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
found = list(joined.where(FederatedLogin.service_ident == password,
|
||||
|
@ -204,8 +217,9 @@ def convert_user_to_organization(user, admin_user):
|
|||
|
||||
|
||||
def create_team(name, org, team_role_name, description=''):
|
||||
if not validate_username(name):
|
||||
raise InvalidTeamException('Invalid team name: %s' % name)
|
||||
(username_valid, username_issue) = validate_username(name)
|
||||
if not username_valid:
|
||||
raise InvalidTeamException('Invalid team name %s: %s' % (name, username_issue))
|
||||
|
||||
if not org.organization:
|
||||
raise InvalidOrganizationException('User with name %s is not an org.' %
|
||||
|
@ -1443,11 +1457,41 @@ def get_recent_repository_build(namespace_name, repository_name):
|
|||
|
||||
|
||||
def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
|
||||
display_name, trigger=None):
|
||||
display_name, trigger=None, pull_robot_name=None):
|
||||
pull_robot = None
|
||||
if pull_robot_name:
|
||||
pull_robot = lookup_robot(pull_robot_name)
|
||||
|
||||
return RepositoryBuild.create(repository=repo, access_token=access_token,
|
||||
job_config=json.dumps(job_config_obj),
|
||||
display_name=display_name, trigger=trigger,
|
||||
resource_key=dockerfile_id)
|
||||
resource_key=dockerfile_id,
|
||||
pull_robot=pull_robot)
|
||||
|
||||
|
||||
def get_pull_robot_name(trigger):
|
||||
if not trigger.pull_robot:
|
||||
return None
|
||||
|
||||
return trigger.pull_robot.username
|
||||
|
||||
|
||||
def get_pull_credentials(robotname):
|
||||
robot = lookup_robot(robotname)
|
||||
if not robot:
|
||||
return None
|
||||
|
||||
try:
|
||||
login_info = FederatedLogin.get(user=robot)
|
||||
except FederatedLogin.DoesNotExist:
|
||||
return None
|
||||
|
||||
return {
|
||||
'username': robot.username,
|
||||
'password': login_info.service_ident,
|
||||
'registry': '%s://%s/v1/' % (app.config['PREFERRED_URL_SCHEME'],
|
||||
app.config['SERVER_HOSTNAME']),
|
||||
}
|
||||
|
||||
|
||||
def create_webhook(repo, params_obj):
|
||||
|
@ -1478,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)
|
||||
|
@ -1487,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())
|
||||
|
||||
|
@ -1506,11 +1551,12 @@ def log_action(kind_name, user_or_organization_name, performer=None,
|
|||
metadata_json=json.dumps(metadata), datetime=timestamp)
|
||||
|
||||
|
||||
def create_build_trigger(repo, service_name, auth_token, user):
|
||||
def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None):
|
||||
service = BuildTriggerService.get(name=service_name)
|
||||
trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
|
||||
auth_token=auth_token,
|
||||
connected_user=user)
|
||||
connected_user=user,
|
||||
pull_robot=pull_robot)
|
||||
return trigger
|
||||
|
||||
|
||||
|
@ -1589,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
|
||||
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,130 @@ 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)
|
||||
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)
|
||||
|
||||
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())
|
||||
self.store_stream(file_like_obj, content_type)
|
||||
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 = {
|
||||
|
@ -220,7 +225,7 @@ class UserPlan(ApiResource):
|
|||
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)
|
||||
|
@ -234,6 +239,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 = {
|
||||
|
@ -279,7 +285,7 @@ class OrganizationPlan(ApiResource):
|
|||
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)
|
||||
|
@ -294,6 +300,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 +317,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 +331,4 @@ class OrgnaizationInvoiceList(ApiResource):
|
|||
|
||||
return get_invoices(organization.stripe_id)
|
||||
|
||||
raise Unauthorized()
|
||||
raise Unauthorized()
|
||||
|
|
|
@ -3,19 +3,20 @@ 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)
|
||||
from endpoints.common import start_build
|
||||
from endpoints.trigger import BuildTrigger
|
||||
from data import model
|
||||
from auth.permissions import ModifyRepositoryPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
|
||||
from data.buildlogs import BuildStatusRetrievalError
|
||||
from util.names import parse_robot_username
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
user_files = app.config['USERFILES']
|
||||
build_logs = app.config['BUILDLOGS']
|
||||
|
||||
|
||||
|
@ -33,7 +34,15 @@ def get_job_config(build_obj):
|
|||
return None
|
||||
|
||||
|
||||
def user_view(user):
|
||||
return {
|
||||
'name': user.username,
|
||||
'kind': 'user',
|
||||
'is_robot': user.robot,
|
||||
}
|
||||
|
||||
def trigger_view(trigger):
|
||||
|
||||
if trigger and trigger.uuid:
|
||||
config_dict = get_trigger_config(trigger)
|
||||
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
|
||||
|
@ -42,7 +51,8 @@ def trigger_view(trigger):
|
|||
'config': config_dict,
|
||||
'id': trigger.uuid,
|
||||
'connected_user': trigger.connected_user.username,
|
||||
'is_active': build_trigger.is_active(config_dict)
|
||||
'is_active': build_trigger.is_active(config_dict),
|
||||
'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
|
||||
}
|
||||
|
||||
return None
|
||||
|
@ -67,6 +77,7 @@ def build_status_view(build_obj, can_write=False):
|
|||
'is_writer': can_write,
|
||||
'trigger': trigger_view(build_obj.trigger),
|
||||
'resource_key': build_obj.resource_key,
|
||||
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
|
||||
}
|
||||
|
||||
if can_write:
|
||||
|
@ -95,6 +106,10 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
'type': 'string',
|
||||
'description': 'Subdirectory in which the Dockerfile can be found',
|
||||
},
|
||||
'pull_robot': {
|
||||
'type': 'string',
|
||||
'description': 'Username of a Quay robot account to use as pull credentials',
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -123,6 +138,22 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
|
||||
dockerfile_id = request_json['file_id']
|
||||
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
|
||||
pull_robot_name = request_json.get('pull_robot', None)
|
||||
|
||||
# Verify the security behind the pull robot.
|
||||
if pull_robot_name:
|
||||
result = parse_robot_username(pull_robot_name)
|
||||
if result:
|
||||
pull_robot = model.lookup_robot(pull_robot_name)
|
||||
if not pull_robot:
|
||||
raise NotFound()
|
||||
|
||||
# Make sure the user has administer permissions for the robot's namespace.
|
||||
(robot_namespace, shortname) = result
|
||||
if not AdministerOrganizationPermission(robot_namespace).can():
|
||||
raise Unauthorized()
|
||||
else:
|
||||
raise Unauthorized()
|
||||
|
||||
# Check if the dockerfile resource has already been used. If so, then it
|
||||
# can only be reused if the user has access to the repository for which it
|
||||
|
@ -137,7 +168,8 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
repo = model.get_repository(namespace, repository)
|
||||
display_name = user_files.get_file_checksum(dockerfile_id)
|
||||
|
||||
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True)
|
||||
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True,
|
||||
pull_robot_name=pull_robot_name)
|
||||
|
||||
resp = build_status_view(build_request, True)
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
@ -24,6 +26,9 @@ def subscription_view(stripe_subscription, used_repos):
|
|||
|
||||
|
||||
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 +61,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 +74,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)
|
|
@ -16,7 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
|
|||
TriggerActivationException, EmptyRepositoryException,
|
||||
RepositoryReadException)
|
||||
from data import model
|
||||
from auth.permissions import UserAdminPermission
|
||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
||||
from util.names import parse_robot_username
|
||||
from util.dockerfileparse import parse_dockerfile
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -139,7 +141,19 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
'BuildTriggerActivateRequest': {
|
||||
'id': 'BuildTriggerActivateRequest',
|
||||
'type': 'object',
|
||||
'description': 'Arbitrary json.',
|
||||
'required': [
|
||||
'config'
|
||||
],
|
||||
'properties': {
|
||||
'config': {
|
||||
'type': 'object',
|
||||
'description': 'Arbitrary json.',
|
||||
},
|
||||
'pull_robot': {
|
||||
'type': 'string',
|
||||
'description': 'The name of the robot that will be used to pull images.'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -160,7 +174,27 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
|
||||
user_permission = UserAdminPermission(trigger.connected_user.username)
|
||||
if user_permission.can():
|
||||
new_config_dict = request.get_json()
|
||||
# Update the pull robot (if any).
|
||||
pull_robot_name = request.get_json().get('pull_robot', None)
|
||||
if pull_robot_name:
|
||||
pull_robot = model.lookup_robot(pull_robot_name)
|
||||
if not pull_robot:
|
||||
raise NotFound()
|
||||
|
||||
# Make sure the user has administer permissions for the robot's namespace.
|
||||
(robot_namespace, shortname) = parse_robot_username(pull_robot_name)
|
||||
if not AdministerOrganizationPermission(robot_namespace).can():
|
||||
raise Unauthorized()
|
||||
|
||||
# Make sure the namespace matches that of the trigger.
|
||||
if robot_namespace != namespace:
|
||||
raise Unauthorized()
|
||||
|
||||
# Set the pull robot.
|
||||
trigger.pull_robot = pull_robot
|
||||
|
||||
# Update the config.
|
||||
new_config_dict = request.get_json()['config']
|
||||
|
||||
token_name = 'Build Trigger: %s' % trigger.service.name
|
||||
token = model.create_delegate_token(namespace, repository, token_name,
|
||||
|
@ -171,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)
|
||||
|
@ -191,6 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
log_action('setup_repo_trigger', namespace,
|
||||
{'repo': repository, 'namespace': namespace,
|
||||
'trigger_id': trigger.uuid, 'service': trigger.service.name,
|
||||
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
|
||||
'config': final_config}, repo=repo)
|
||||
|
||||
return trigger_view(trigger)
|
||||
|
@ -198,6 +232,141 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@internal_only
|
||||
class BuildTriggerAnalyze(RepositoryParamResource):
|
||||
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
||||
(such as a robot account to use for pulling)
|
||||
"""
|
||||
schemas = {
|
||||
'BuildTriggerAnalyzeRequest': {
|
||||
'id': 'BuildTriggerAnalyzeRequest',
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'config'
|
||||
],
|
||||
'properties': {
|
||||
'config': {
|
||||
'type': 'object',
|
||||
'description': 'Arbitrary json.',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('analyzeBuildTrigger')
|
||||
@validate_json_request('BuildTriggerAnalyzeRequest')
|
||||
def post(self, namespace, repository, trigger_uuid):
|
||||
""" Analyze the specified build trigger configuration. """
|
||||
try:
|
||||
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||
except model.InvalidBuildTriggerException:
|
||||
raise NotFound()
|
||||
|
||||
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
|
||||
new_config_dict = request.get_json()['config']
|
||||
|
||||
try:
|
||||
# Load the contents of the Dockerfile.
|
||||
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict)
|
||||
if not contents:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not read the Dockerfile for the trigger'
|
||||
}
|
||||
|
||||
# Parse the contents of the Dockerfile.
|
||||
parsed = parse_dockerfile(contents)
|
||||
if not parsed:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not parse the Dockerfile specified'
|
||||
}
|
||||
|
||||
# Determine the base image (i.e. the FROM) for the Dockerfile.
|
||||
base_image = parsed.get_base_image()
|
||||
if not base_image:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': 'No FROM line found in the Dockerfile'
|
||||
}
|
||||
|
||||
# Check to see if the base image lives in Quay.
|
||||
quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME'])
|
||||
|
||||
if not base_image.startswith(quay_registry_prefix):
|
||||
return {
|
||||
'status': 'publicbase'
|
||||
}
|
||||
|
||||
# Lookup the repository in Quay.
|
||||
result = base_image[len(quay_registry_prefix):].split('/', 2)
|
||||
if len(result) != 2:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': '"%s" is not a valid Quay repository path' % (base_image)
|
||||
}
|
||||
|
||||
(base_namespace, base_repository) = result
|
||||
found_repository = model.get_repository(base_namespace, base_repository)
|
||||
if not found_repository:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# If the repository is private and the user cannot see that repo, then
|
||||
# mark it as not found.
|
||||
can_read = ReadRepositoryPermission(base_namespace, base_repository)
|
||||
if found_repository.visibility.name != 'public' and not can_read:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# Check to see if the repository is public. If not, we suggest the
|
||||
# usage of a robot account to conduct the pull.
|
||||
read_robots = []
|
||||
|
||||
if AdministerOrganizationPermission(base_namespace).can():
|
||||
def robot_view(robot):
|
||||
return {
|
||||
'name': robot.username,
|
||||
'kind': 'user',
|
||||
'is_robot': True
|
||||
}
|
||||
|
||||
def is_valid_robot(user):
|
||||
# Make sure the user is a robot.
|
||||
if not user.robot:
|
||||
return False
|
||||
|
||||
# Make sure the current user can see/administer the robot.
|
||||
(robot_namespace, shortname) = parse_robot_username(user.username)
|
||||
return AdministerOrganizationPermission(robot_namespace).can()
|
||||
|
||||
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
|
||||
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
|
||||
|
||||
return {
|
||||
'namespace': base_namespace,
|
||||
'name': base_repository,
|
||||
'is_public': found_repository.visibility.name == 'public',
|
||||
'robots': read_robots,
|
||||
'status': 'analyzed',
|
||||
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict)
|
||||
}
|
||||
|
||||
except RepositoryReadException as rre:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': rre.message
|
||||
}
|
||||
|
||||
raise NotFound()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
""" Custom verb to manually activate a build trigger. """
|
||||
|
@ -220,8 +389,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
dockerfile_id, tags, name, subdir = specs
|
||||
|
||||
repo = model.get_repository(namespace, repository)
|
||||
pull_robot_name = model.get_pull_robot_name(trigger)
|
||||
|
||||
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True)
|
||||
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
|
||||
pull_robot_name=pull_robot_name)
|
||||
|
||||
resp = build_status_view(build_request, True)
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
@ -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'))
|
||||
|
|
|
@ -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
|
||||
|
@ -91,7 +117,14 @@ def random_string():
|
|||
|
||||
def render_page_template(name, **kwargs):
|
||||
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()),
|
||||
cache_buster='foobarbaz', **kwargs))
|
||||
feature_set=json.dumps(features.get_features()),
|
||||
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
|
||||
show_chat=features.OLARK_CHAT,
|
||||
cache_buster=random_string(),
|
||||
**kwargs))
|
||||
|
||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||
return resp
|
||||
|
||||
|
@ -107,7 +140,7 @@ def check_repository_usage(user_or_org, plan_found):
|
|||
|
||||
|
||||
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||
trigger=None):
|
||||
trigger=None, pull_robot_name=None):
|
||||
host = urlparse.urlparse(request.url).netloc
|
||||
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
|
||||
|
||||
|
@ -118,16 +151,18 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
|||
job_config = {
|
||||
'docker_tags': tags,
|
||||
'repository': repo_path,
|
||||
'build_subdir': subdir,
|
||||
'build_subdir': subdir
|
||||
}
|
||||
|
||||
build_request = model.create_repository_build(repository, token, job_config,
|
||||
dockerfile_id, build_name,
|
||||
trigger)
|
||||
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,
|
||||
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
|
||||
}), retries_remaining=1)
|
||||
|
||||
metadata = {
|
||||
|
|
|
@ -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
|
||||
|
@ -227,7 +227,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 +236,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 +250,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
|
||||
|
||||
|
@ -315,7 +315,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)
|
||||
|
||||
|
@ -374,7 +374,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,
|
||||
|
|
|
@ -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,9 +17,9 @@ from auth.permissions import (ReadRepositoryPermission,
|
|||
ModifyRepositoryPermission)
|
||||
from data import model
|
||||
|
||||
|
||||
registry = Blueprint('registry', __name__)
|
||||
|
||||
store = app.config['STORAGE']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -179,7 +179,7 @@ def put_image_layer(namespace, repository, image_id):
|
|||
# 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({
|
||||
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||
'namespace': namespace,
|
||||
'repository': repository,
|
||||
'image_id': image_id,
|
||||
|
@ -232,7 +232,7 @@ def put_image_checksum(namespace, repository, image_id):
|
|||
# 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({
|
||||
image_diff_queue.put([namespace, repository, image_id], json.dumps({
|
||||
'namespace': namespace,
|
||||
'repository': repository,
|
||||
'image_id': image_id,
|
||||
|
|
|
@ -2,14 +2,14 @@ import logging
|
|||
import io
|
||||
import os.path
|
||||
import tarfile
|
||||
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']
|
||||
|
||||
|
||||
|
@ -46,6 +46,19 @@ class BuildTrigger(object):
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
"""
|
||||
Returns the URL at which the Dockerfile for the trigger can be found or None if none/not applicable.
|
||||
"""
|
||||
return None
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
"""
|
||||
Loads the Dockerfile found for the trigger's config and returns them or None if none could
|
||||
be found/loaded.
|
||||
"""
|
||||
return None
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
"""
|
||||
Take the auth information for the specific trigger type and load the
|
||||
|
@ -168,7 +181,6 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
return config
|
||||
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
gh_client = self._get_client(auth_token)
|
||||
usr = gh_client.get_user()
|
||||
|
@ -219,6 +231,41 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
source = config['build_source']
|
||||
subdirectory = config.get('subdir', '')
|
||||
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
|
||||
|
||||
gh_client = self._get_client(auth_token)
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
master_branch = repo.master_branch or 'master'
|
||||
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
|
||||
except GithubException as ge:
|
||||
return None
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
gh_client = self._get_client(auth_token)
|
||||
|
||||
source = config['build_source']
|
||||
subdirectory = config.get('subdir', '')
|
||||
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
|
||||
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
file_info = repo.get_file_contents(path)
|
||||
if file_info is None:
|
||||
return None
|
||||
|
||||
content = file_info.content
|
||||
if file_info.encoding == 'base64':
|
||||
content = base64.b64decode(content)
|
||||
return content
|
||||
|
||||
except GithubException as ge:
|
||||
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
||||
# Prepare the download and upload URLs
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
import stripe
|
||||
import os
|
||||
|
||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||
|
@ -9,17 +8,20 @@ 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__)
|
||||
|
@ -54,6 +56,7 @@ def snapshot(path = ''):
|
|||
|
||||
@web.route('/plans/')
|
||||
@no_cache
|
||||
@route_show_if(features.BILLING)
|
||||
def plans():
|
||||
return index('')
|
||||
|
||||
|
@ -82,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,6 +161,8 @@ def privacy():
|
|||
|
||||
|
||||
@web.route('/receipt', methods=['GET'])
|
||||
@route_show_if(features.BILLING)
|
||||
@require_session_login
|
||||
def receipt():
|
||||
if not current_user.is_authenticated():
|
||||
abort(401)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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
|
||||
|
@ -73,8 +72,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')
|
||||
|
||||
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)
|
||||
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
|
||||
pull_robot_name=pull_robot_name)
|
||||
|
||||
return make_response('Okay')
|
||||
|
||||
|
|
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__()
|
||||
|
||||
|
19
initdb.py
19
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
|
||||
|
@ -243,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)
|
||||
|
@ -259,7 +256,7 @@ def populate_database():
|
|||
new_user_1.stripe_id = TEST_STRIPE_ID
|
||||
new_user_1.save()
|
||||
|
||||
model.create_robot('dtrobot', new_user_1)
|
||||
dtrobot = model.create_robot('dtrobot', new_user_1)
|
||||
|
||||
new_user_2 = model.create_user('public', 'password',
|
||||
'jacob.moshenko@gmail.com')
|
||||
|
@ -270,6 +267,8 @@ def populate_database():
|
|||
new_user_3.verified = True
|
||||
new_user_3.save()
|
||||
|
||||
model.create_robot('anotherrobot', new_user_3)
|
||||
|
||||
new_user_4 = model.create_user('randomuser', 'password', 'no4@thanks.com')
|
||||
new_user_4.verified = True
|
||||
new_user_4.save()
|
||||
|
@ -295,7 +294,7 @@ def populate_database():
|
|||
|
||||
__generate_repository(new_user_1, 'complex',
|
||||
'Complex repository with many branches and tags.',
|
||||
False, [(new_user_2, 'read')],
|
||||
False, [(new_user_2, 'read'), (dtrobot[0], 'read')],
|
||||
(2, [(3, [], 'v2.0'),
|
||||
(1, [(1, [(1, [], ['prod'])],
|
||||
'staging'),
|
||||
|
@ -332,7 +331,7 @@ def populate_database():
|
|||
token = model.create_access_token(building, 'write')
|
||||
|
||||
trigger = model.create_build_trigger(building, 'github', '123authtoken',
|
||||
new_user_1)
|
||||
new_user_1, pull_robot=dtrobot[0])
|
||||
trigger.config = json.dumps({
|
||||
'build_source': 'jakedt/testconnect',
|
||||
'subdir': '',
|
||||
|
@ -368,6 +367,8 @@ def populate_database():
|
|||
org.stripe_id = TEST_STRIPE_ID
|
||||
org.save()
|
||||
|
||||
model.create_robot('coolrobot', org)
|
||||
|
||||
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
|
||||
client_id='deadbeef')
|
||||
|
||||
|
|
|
@ -21,9 +21,12 @@ xhtml2pdf
|
|||
logstash_formatter
|
||||
redis
|
||||
hiredis
|
||||
docker-py
|
||||
git+https://github.com/DevTable/docker-py.git
|
||||
loremipsum
|
||||
pygithub
|
||||
flask-restful
|
||||
jsonschema
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
git+https://github.com/NateFerrero/oauth2lib.git
|
||||
alembic
|
||||
sqlalchemy
|
||||
python-magic
|
||||
|
|
|
@ -5,25 +5,27 @@ Flask-Mail==0.9.0
|
|||
Flask-Principal==0.4.0
|
||||
Flask-RESTful==0.2.12
|
||||
Jinja2==2.7.2
|
||||
Mako==0.9.1
|
||||
MarkupSafe==0.19
|
||||
Pillow==2.3.1
|
||||
Pillow==2.4.0
|
||||
PyGithub==1.24.1
|
||||
PyMySQL==0.6.1
|
||||
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
|
||||
docker-py==0.3.0
|
||||
git+https://github.com/DevTable/docker-py.git
|
||||
ecdsa==0.11
|
||||
gevent==1.0
|
||||
greenlet==0.4.2
|
||||
gunicorn==18.0
|
||||
hiredis==0.1.2
|
||||
html5lib==1.0b3
|
||||
itsdangerous==0.23
|
||||
itsdangerous==0.24
|
||||
jsonschema==2.3.0
|
||||
lockfile==0.9.1
|
||||
logstash-formatter==0.5.8
|
||||
|
@ -40,12 +42,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
|
||||
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
|
||||
|
|
|
@ -2445,11 +2445,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;
|
||||
}
|
||||
|
@ -2540,7 +2535,7 @@ p.editable:hover i {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#image-history-container .tags .tag, .tag-specific-images-view .tag {
|
||||
.tags .tag, .tag-specific-images-view .tag {
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
margin-right: 4px;
|
||||
|
@ -2549,6 +2544,13 @@ p.editable:hover i {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tooltip-tags {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
border-top: 1px dotted #aaa;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#changes-tree-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -2589,42 +2591,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;
|
||||
|
@ -3588,7 +3590,7 @@ pre.command:before {
|
|||
|
||||
position: relative;
|
||||
|
||||
height: 100px;
|
||||
height: 75px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -3721,4 +3723,51 @@ pre.command:before {
|
|||
.auth-info .scope {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.trigger-pull-credentials {
|
||||
margin-top: 4px;
|
||||
padding-left: 26px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trigger-pull-credentials .context-tooltip {
|
||||
color: gray;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.trigger-description .trigger-description-subtitle {
|
||||
display: inline-block;
|
||||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.trigger-option-section:not(:first-child) {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.trigger-option-section .entity-search-element .twitter-typeahead {
|
||||
width: 370px;
|
||||
}
|
||||
|
||||
.trigger-option-section .entity-search-element input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
|
@ -30,7 +30,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="id-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||
<span class="input-group-addon" title="Copy to Clipboard">
|
||||
<span class="input-group-addon" data-title="Copy to Clipboard">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<span class="delete-ui-element" ng-click="focus()">
|
||||
<span class="delete-ui-button" ng-click="performDelete()"><button class="btn btn-danger">{{ buttonTitleInternal }}</button></span>
|
||||
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" title="{{ deleteTitle }}"></i>
|
||||
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" data-title="{{ deleteTitle }}"></i>
|
||||
</span>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<div class="container" ng-show="!uploading && !building">
|
||||
<div class="init-description">
|
||||
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||
Upload a <b>Dockerfile</b> or an archive (<code>.zip</code> or <code>.tar.gz</code>) containing a Dockerfile <b>in the root directory</b>
|
||||
</div>
|
||||
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<span class="entity-reference-element">
|
||||
<span ng-if="entity.kind == 'team'">
|
||||
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-group" data-title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<span class="entity-name">
|
||||
<span ng-if="!getIsAdmin(namespace)">{{entity.name}}</span>
|
||||
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="entity.kind != 'team'">
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<span class="entity-name" ng-if="entity.is_robot">
|
||||
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
||||
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
||||
|
@ -22,6 +22,6 @@
|
|||
</span>
|
||||
</span>
|
||||
<i class="fa fa-exclamation-triangle" ng-if="entity.is_org_member === false"
|
||||
title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
|
||||
data-title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
|
||||
</i>
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntity" entity="currentEntity"></span>
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntityInternal" entity="currentEntityInternal"></span>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
||||
ng-click="lazyLoad()">
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
|||
</form>
|
||||
|
||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
|
||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" data-title="Create new repository"></i></a>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
|||
ng-show="notificationService.notifications.length"
|
||||
ng-class="notificationService.notificationClasses"
|
||||
bs-tooltip=""
|
||||
title="User Notifications"
|
||||
data-title="User Notifications"
|
||||
data-placement="left"
|
||||
data-container="body">
|
||||
{{ notificationService.notifications.length }}
|
||||
|
@ -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>
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
</span>
|
||||
<span class="right">
|
||||
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
||||
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
<a href="{{ logsPath }}" download="usage-log.json" target="_new">
|
||||
<i class="fa fa-download toggle-icon" title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-download toggle-icon" data-title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -55,7 +55,7 @@
|
|||
<td>
|
||||
<span class="log-performer" ng-if="log.metadata.oauth_token_application">
|
||||
<div>
|
||||
<span class="application-reference" title="log.metadata.oauth_token_application"
|
||||
<span class="application-reference" data-title="log.metadata.oauth_token_application"
|
||||
client-id="log.metadata.oauth_token_application_id"></span>
|
||||
</div>
|
||||
<div style="text-align: center; font-size: 12px; color: #aaa; padding: 4px;">on behalf of</div>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</a>
|
||||
|
||||
<i class="fa fa-exclamation-triangle" ng-show="requireCreate && !namespaces[org.name].can_create_repo"
|
||||
title="You do not have permission to create repositories for this organization"
|
||||
data-title="You do not have permission to create repositories for this organization"
|
||||
data-placement="right"
|
||||
bs-tooltip="tooltip.title"></i>
|
||||
</li>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<!-- 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>
|
||||
|
||||
|
@ -38,7 +38,7 @@
|
|||
<td>
|
||||
{{ plan.title }}
|
||||
<div class="deprecated-plan-label" ng-show="plan.deprecated">
|
||||
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||
<span class="context-tooltip" data-title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ plan.privateRepos }}</td>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<div class="container" ng-show="!loading">
|
||||
<div class="alert alert-info">
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
|
||||
</div>
|
||||
|
||||
<div class="side-controls">
|
||||
|
@ -17,13 +17,13 @@
|
|||
<thead>
|
||||
<th>
|
||||
<span class="context-tooltip"
|
||||
title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
|
||||
data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Repository Creator
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="context-tooltip" title="The user, robot or team that is being granted the permission"
|
||||
<span class="context-tooltip" data-title="The user, robot or team that is being granted the permission"
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Applies To User/Robot/Team
|
||||
</span>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" title="Private Repository"></i>
|
||||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" data-title="Private Repository"></i>
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
|
|
100
static/directives/setup-trigger-dialog.html
Normal file
100
static/directives/setup-trigger-dialog.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
<div class="setup-trigger-directive-element">
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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">
|
||||
<!-- Trigger-specific setup -->
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repository" trigger="trigger"
|
||||
analyze="checkAnalyze(isValid)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pull information -->
|
||||
<div class="trigger-option-section" ng-show="showPullRequirements">
|
||||
<div ng-show="!pullRequirements">
|
||||
<span class="quay-spinner"></span> Checking pull credential requirements...
|
||||
</div>
|
||||
|
||||
<div ng-show="pullRequirements">
|
||||
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false">
|
||||
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
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
|
||||
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
||||
</a> which requires
|
||||
a robot account for pull access, because it is marked <strong>private</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
Pull Credentials:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
|
||||
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repository.namespace }}</strong>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">Public</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)">
|
||||
<i class="fa fa-wrench"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="!publicPull">
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div class="entity-search" namespace="repository.namespace" include-teams="false"
|
||||
input-title="'Select robot account for pulling...'"
|
||||
is-organization="repository.is_organization"
|
||||
is-persistent="true"
|
||||
current-entity="pullEntity"
|
||||
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.
|
||||
</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
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
|
||||
ng-click="activate">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
|
@ -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>
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
<div class="signup-form-element">
|
||||
<form class="form-signup" name="signupForm" ng-submit="register()" data-trigger="manual"
|
||||
data-content="{{ registerError }}" data-placement="left" ng-show="!awaitingConfirmation && !registering">
|
||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required>
|
||||
<form class="form-signup" name="signupForm" ng-submit="register()" ngshow="!awaitingConfirmation && !registering">
|
||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
|
||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword"
|
||||
match="newUser.password" required>
|
||||
match="newUser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
<div class="form-group signin-buttons">
|
||||
<button 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">
|
||||
<button id="signupButton"
|
||||
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||
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">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
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="" title="getFirstTextLine(image.comment)"
|
||||
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="getFirstTextLine(image.comment)"
|
||||
data-html="true">
|
||||
{{ image.id.substr(0, 12) }}
|
||||
</span>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<span class="trigger-description-element" ng-switch on="trigger.service">
|
||||
<span ng-switch-when="github">
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px" title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a>
|
||||
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="trigger.config.subdir">
|
||||
<span>Dockerfile:
|
||||
|
@ -10,7 +10,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short">
|
||||
<span>Dockerfile:
|
||||
<span><span class="trigger-description-subtitle">Dockerfile:</span>
|
||||
<a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/Dockerfile" target="_blank">
|
||||
//Dockerfile
|
||||
</a>
|
||||
|
|
631
static/js/app.js
631
static/js/app.js
|
@ -102,7 +102,17 @@ function getMarkedDown(string) {
|
|||
return Markdown.getSanitizingConverter().makeHtml(string || '');
|
||||
}
|
||||
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) {
|
||||
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
|
||||
'ngAnimate'];
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayDependencies.push('angulartics');
|
||||
quayDependencies.push('angulartics.mixpanel');
|
||||
}
|
||||
|
||||
quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
||||
/**
|
||||
|
@ -325,6 +335,42 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
}]);
|
||||
|
||||
|
||||
$provide.factory('UIService', [function() {
|
||||
var uiService = {};
|
||||
|
||||
uiService.hidePopover = function(elem) {
|
||||
var popover = $('#signupButton').data('bs.popover');
|
||||
if (popover) {
|
||||
popover.hide();
|
||||
}
|
||||
};
|
||||
|
||||
uiService.showPopover = function(elem, content) {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
if (!popover) {
|
||||
$(elem).popover({'content': '-', 'placement': 'left'});
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
popover.options.content = content;
|
||||
popover.show();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
uiService.showFormError = function(elem, result) {
|
||||
var message = result.data['message'] || result.data['error_description'] || '';
|
||||
if (message) {
|
||||
uiService.showPopover(elem, message);
|
||||
} else {
|
||||
uiService.hidePopover(elem);
|
||||
}
|
||||
};
|
||||
|
||||
return uiService;
|
||||
}]);
|
||||
|
||||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
var utilService = {};
|
||||
|
||||
|
@ -439,6 +485,63 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
return metadataService;
|
||||
}]);
|
||||
|
||||
$provide.factory('Features', [function() {
|
||||
if (!window.__features) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var features = window.__features;
|
||||
features.getFeature = function(name, opt_defaultValue) {
|
||||
var value = features[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
features.hasFeature = function(name) {
|
||||
return !!features.getFeature(name);
|
||||
};
|
||||
|
||||
features.matchesFeatures = function(list) {
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
var value = features.getFeature(list[i]);
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return features;
|
||||
}]);
|
||||
|
||||
$provide.factory('Config', [function() {
|
||||
if (!window.__config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var config = window.__config;
|
||||
config.getDomain = function() {
|
||||
return config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getUrl = function(opt_path) {
|
||||
var path = opt_path || '';
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
|
||||
};
|
||||
|
||||
config.getValue = function(name, opt_defaultValue) {
|
||||
var value = config[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return config;
|
||||
}]);
|
||||
|
||||
$provide.factory('ApiService', ['Restangular', function(Restangular) {
|
||||
var apiService = {};
|
||||
|
||||
|
@ -622,8 +725,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
return cookieService;
|
||||
}]);
|
||||
|
||||
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope',
|
||||
function(ApiService, CookieService, $rootScope) {
|
||||
$provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
|
||||
function(ApiService, CookieService, $rootScope, Config) {
|
||||
var userResponse = {
|
||||
verified: false,
|
||||
anonymous: true,
|
||||
|
@ -653,15 +756,17 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
userResponse = loadedUser;
|
||||
|
||||
if (!userResponse.anonymous) {
|
||||
mixpanel.identify(userResponse.username);
|
||||
mixpanel.people.set({
|
||||
'$email': userResponse.email,
|
||||
'$username': userResponse.username,
|
||||
'verified': userResponse.verified
|
||||
});
|
||||
mixpanel.people.set_once({
|
||||
'$created': new Date()
|
||||
})
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.identify(userResponse.username);
|
||||
mixpanel.people.set({
|
||||
'$email': userResponse.email,
|
||||
'$username': userResponse.username,
|
||||
'verified': userResponse.verified
|
||||
});
|
||||
mixpanel.people.set_once({
|
||||
'$created': new Date()
|
||||
})
|
||||
}
|
||||
|
||||
if (window.olark !== undefined) {
|
||||
olark('api.visitor.getDetails', function(details) {
|
||||
|
@ -735,8 +840,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
return userService;
|
||||
}]);
|
||||
|
||||
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService',
|
||||
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) {
|
||||
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
|
||||
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
|
||||
var notificationService = {
|
||||
'user': null,
|
||||
'notifications': [],
|
||||
|
@ -830,28 +935,18 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
return notificationService;
|
||||
}]);
|
||||
|
||||
$provide.factory('KeyService', ['$location', function($location) {
|
||||
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
|
||||
var keyService = {}
|
||||
|
||||
if ($location.host() === 'quay.io') {
|
||||
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
|
||||
keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
|
||||
keyService['githubRedirectUri'] = 'https://quay.io/oauth2/github/callback';
|
||||
} else if($location.host() === 'staging.quay.io') {
|
||||
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
|
||||
keyService['githubClientId'] = '4886304accbc444f0471';
|
||||
keyService['githubRedirectUri'] = 'https://staging.quay.io/oauth2/github/callback';
|
||||
} else {
|
||||
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
|
||||
keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
|
||||
keyService['githubRedirectUri'] = 'http://localhost:5000/oauth2/github/callback';
|
||||
}
|
||||
|
||||
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
|
||||
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
|
||||
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
|
||||
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
|
||||
return keyService;
|
||||
}]);
|
||||
|
||||
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
|
||||
function(KeyService, UserService, CookieService, ApiService) {
|
||||
|
||||
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
|
||||
function(KeyService, UserService, CookieService, ApiService, Features, Config) {
|
||||
var plans = null;
|
||||
var planDict = {};
|
||||
var planService = {};
|
||||
|
@ -877,7 +972,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.notePlan = function(planId) {
|
||||
CookieService.putSession('quay.notedplan', planId);
|
||||
if (Features.BILLING) {
|
||||
CookieService.putSession('quay.notedplan', planId);
|
||||
}
|
||||
};
|
||||
|
||||
planService.isOrgCompatible = function(plan) {
|
||||
|
@ -903,7 +1000,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
|
||||
planService.handleNotedPlan = function() {
|
||||
var planId = planService.getAndResetNotedPlan();
|
||||
if (!planId) { return false; }
|
||||
if (!planId || !Features.BILLING) { return false; }
|
||||
|
||||
UserService.load(function() {
|
||||
if (UserService.currentUser().anonymous) {
|
||||
|
@ -948,6 +1045,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.verifyLoaded = function(callback) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
if (plans) {
|
||||
callback(plans);
|
||||
return;
|
||||
|
@ -1007,10 +1106,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.getSubscription = function(orgname, success, failure) {
|
||||
ApiService.getSubscription(orgname).then(success, failure);
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
ApiService.getSubscription(orgname).then(success, failure);
|
||||
};
|
||||
|
||||
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
var subscriptionDetails = {
|
||||
plan: planId
|
||||
};
|
||||
|
@ -1030,6 +1133,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.getCardInfo = function(orgname, callback) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
ApiService.getCard(orgname).then(function(resp) {
|
||||
callback(resp.card);
|
||||
}, function() {
|
||||
|
@ -1038,6 +1143,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.changePlan = function($scope, orgname, planId, callbacks) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
if (callbacks['started']) {
|
||||
callbacks['started']();
|
||||
}
|
||||
|
@ -1063,6 +1170,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.changeCreditCard = function($scope, orgname, callbacks) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
if (callbacks['opening']) {
|
||||
callbacks['opening']();
|
||||
}
|
||||
|
@ -1119,6 +1228,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
};
|
||||
|
||||
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
if (callbacks['opening']) {
|
||||
callbacks['opening']();
|
||||
}
|
||||
|
@ -1128,7 +1239,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
if (submitted) { return; }
|
||||
submitted = true;
|
||||
|
||||
mixpanel.track('plan_subscribe');
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.track('plan_subscribe');
|
||||
}
|
||||
|
||||
$scope.$apply(function() {
|
||||
if (callbacks['started']) {
|
||||
|
@ -1146,7 +1259,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
email: email,
|
||||
amount: planDetails.price,
|
||||
currency: 'usd',
|
||||
name: 'Quay ' + planDetails.title + ' Subscription',
|
||||
name: 'Quay.io ' + planDetails.title + ' Subscription',
|
||||
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
||||
panelLabel: 'Subscribe',
|
||||
token: submitToken,
|
||||
|
@ -1189,10 +1302,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
});
|
||||
};
|
||||
}).
|
||||
config(['$routeProvider', '$locationProvider', '$analyticsProvider',
|
||||
function($routeProvider, $locationProvider, $analyticsProvider) {
|
||||
|
||||
$analyticsProvider.virtualPageviews(true);
|
||||
config(['$routeProvider', '$locationProvider',
|
||||
function($routeProvider, $locationProvider) {
|
||||
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
|
@ -1213,6 +1324,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
|
||||
reloadOnSearch: false, controller: UserAdminCtrl}).
|
||||
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for Quay.io', templateUrl: '/static/partials/super-user.html',
|
||||
reloadOnSearch: false, controller: SuperUserAdminCtrl}).
|
||||
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html',
|
||||
controller: GuideCtrl}).
|
||||
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html',
|
||||
|
@ -1245,6 +1358,178 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
RestangularProvider.setBaseUrl('/api/v1/');
|
||||
});
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayApp.config(['$analyticsProvider', function($analyticsProvider) {
|
||||
$analyticsProvider.virtualPageviews(true);
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
function buildConditionalLinker($animate, name, evaluator) {
|
||||
// Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
|
||||
return function ($scope, $element, $attr, ctrl, $transclude) {
|
||||
var block;
|
||||
var childScope;
|
||||
var roles;
|
||||
|
||||
$attr.$observe(name, function (value) {
|
||||
if (evaluator($scope.$eval(value))) {
|
||||
if (!childScope) {
|
||||
childScope = $scope.$new();
|
||||
$transclude(childScope, function (clone) {
|
||||
block = {
|
||||
startNode: clone[0],
|
||||
endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ')
|
||||
};
|
||||
$animate.enter(clone, $element.parent(), $element);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (childScope) {
|
||||
childScope.$destroy();
|
||||
childScope = null;
|
||||
}
|
||||
|
||||
if (block) {
|
||||
$animate.leave(getBlockElements(block));
|
||||
block = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
quayApp.directive('quayRequire', function ($animate, Features) {
|
||||
return {
|
||||
transclude: 'element',
|
||||
priority: 600,
|
||||
terminal: true,
|
||||
restrict: 'A',
|
||||
link: buildConditionalLinker($animate, 'quayRequire', function(value) {
|
||||
return Features.matchesFeatures(value);
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('quayShow', function($animate, Features, Config) {
|
||||
return {
|
||||
priority: 590,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
$scope.Features = Features;
|
||||
$scope.Config = Config;
|
||||
$scope.$watch($attr.quayShow, function(result) {
|
||||
$animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('quayClasses', function(Features, Config) {
|
||||
return {
|
||||
priority: 580,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
|
||||
// Borrowed from ngClass.
|
||||
function flattenClasses(classVal) {
|
||||
if(angular.isArray(classVal)) {
|
||||
return classVal.join(' ');
|
||||
} else if (angular.isObject(classVal)) {
|
||||
var classes = [], i = 0;
|
||||
angular.forEach(classVal, function(v, k) {
|
||||
if (v) {
|
||||
classes.push(k);
|
||||
}
|
||||
});
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
return classVal;
|
||||
}
|
||||
|
||||
function removeClass(classVal) {
|
||||
$attr.$removeClass(flattenClasses(classVal));
|
||||
}
|
||||
|
||||
|
||||
function addClass(classVal) {
|
||||
$attr.$addClass(flattenClasses(classVal));
|
||||
}
|
||||
|
||||
$scope.$watch($attr.quayClasses, function(result) {
|
||||
var scopeVals = {
|
||||
'Features': Features,
|
||||
'Config': Config
|
||||
};
|
||||
|
||||
for (var expr in result) {
|
||||
if (!result.hasOwnProperty(expr)) { continue; }
|
||||
|
||||
// Evaluate the expression with the entire features list added.
|
||||
var value = $scope.$eval(expr, scopeVals);
|
||||
if (value) {
|
||||
addClass(result[expr]);
|
||||
} else {
|
||||
removeClass(result[expr]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
|
||||
return {
|
||||
priority: 595,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl) {
|
||||
var getTemplate = function(templateName) {
|
||||
var templateUrl = '/static/partials/' + templateName;
|
||||
return $http.get(templateUrl, {cache: $templateCache});
|
||||
};
|
||||
|
||||
var result = $scope.$eval($attr.quayInclude);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
var scopeVals = {
|
||||
'Features': Features,
|
||||
'Config': Config
|
||||
};
|
||||
|
||||
var templatePath = null;
|
||||
for (var expr in result) {
|
||||
if (!result.hasOwnProperty(expr)) { continue; }
|
||||
|
||||
// Evaluate the expression with the entire features list added.
|
||||
var value = $scope.$eval(expr, scopeVals);
|
||||
if (value) {
|
||||
templatePath = result[expr];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = getTemplate(templatePath).success(function(html) {
|
||||
$element.html(html);
|
||||
}).then(function (response) {
|
||||
$element.replaceWith($compile($element.html())($scope));
|
||||
if ($attr.onload) {
|
||||
$scope.$eval($attr.onload);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('entityReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
|
@ -1516,12 +1801,14 @@ quayApp.directive('signinForm', function () {
|
|||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||
$scope.showGithub = function() {
|
||||
if (!Features.GITHUB_LOGIN) { return; }
|
||||
|
||||
$scope.markStarted();
|
||||
|
||||
var mixpanelDistinctIdClause = '';
|
||||
if (mixpanel.get_distinct_id !== undefined) {
|
||||
if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) {
|
||||
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
|
||||
}
|
||||
|
||||
|
@ -1587,34 +1874,35 @@ quayApp.directive('signupForm', function () {
|
|||
scope: {
|
||||
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
||||
$('.form-signup').popover();
|
||||
|
||||
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
|
||||
var mixpanelId = loadedMixpanel.get_distinct_id();
|
||||
$scope.github_state_clause = '&state=' + mixpanelId;
|
||||
});
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
|
||||
var mixpanelId = loadedMixpanel.get_distinct_id();
|
||||
$scope.github_state_clause = '&state=' + mixpanelId;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
|
||||
$scope.awaitingConfirmation = false;
|
||||
$scope.registering = false;
|
||||
|
||||
|
||||
$scope.register = function() {
|
||||
$('.form-signup').popover('hide');
|
||||
UIService.hidePopover('#signupButton');
|
||||
$scope.registering = true;
|
||||
|
||||
ApiService.createNewUser($scope.newUser).then(function() {
|
||||
$scope.awaitingConfirmation = true;
|
||||
$scope.registering = false;
|
||||
|
||||
mixpanel.alias($scope.newUser.username);
|
||||
$scope.awaitingConfirmation = true;
|
||||
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.alias($scope.newUser.username);
|
||||
}
|
||||
}, function(result) {
|
||||
$scope.registering = false;
|
||||
$scope.registerError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('.form-signup').popover('show');
|
||||
});
|
||||
UIService.showFormError('#signupButton', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -1644,7 +1932,7 @@ quayApp.directive('plansTable', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerAuthDialog', function () {
|
||||
quayApp.directive('dockerAuthDialog', function (Config) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/docker-auth-dialog.html',
|
||||
|
@ -1665,11 +1953,10 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
|
||||
$scope.downloadCfg = function() {
|
||||
var auth = $.base64.encode($scope.username + ":" + $scope.token);
|
||||
config = {
|
||||
"https://quay.io/v1/": {
|
||||
"auth": auth,
|
||||
"email": ""
|
||||
}
|
||||
config = {}
|
||||
config[Config.getUrl('/v1/')] = {
|
||||
"auth": auth,
|
||||
"email": ""
|
||||
};
|
||||
|
||||
var file = JSON.stringify(config, null, ' ');
|
||||
|
@ -2653,11 +2940,13 @@ quayApp.directive('entitySearch', function () {
|
|||
'isOrganization': '=isOrganization',
|
||||
'isPersistent': '=isPersistent',
|
||||
'currentEntity': '=currentEntity',
|
||||
'clearNow': '=clearNow'
|
||||
'clearNow': '=clearNow',
|
||||
'filter': '=filter',
|
||||
},
|
||||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
||||
$scope.lazyLoading = true;
|
||||
$scope.isAdmin = false;
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||
|
@ -2736,20 +3025,27 @@ quayApp.directive('entitySearch', function () {
|
|||
entity['is_org_member'] = true;
|
||||
}
|
||||
|
||||
$scope.setEntityInternal(entity);
|
||||
$scope.setEntityInternal(entity, false);
|
||||
};
|
||||
|
||||
$scope.clearEntityInternal = function() {
|
||||
$scope.currentEntityInternal = null;
|
||||
$scope.currentEntity = null;
|
||||
|
||||
if ($scope.entitySelected) {
|
||||
$scope.entitySelected(null);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setEntityInternal = function(entity) {
|
||||
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
|
||||
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
||||
if (updateTypeahead) {
|
||||
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
|
||||
} else {
|
||||
$(input).val($scope.isPersistent ? entity.name : '');
|
||||
}
|
||||
|
||||
if ($scope.isPersistent) {
|
||||
$scope.currentEntityInternal = entity;
|
||||
$scope.currentEntity = entity;
|
||||
}
|
||||
|
||||
|
@ -2777,6 +3073,19 @@ quayApp.directive('entitySearch', function () {
|
|||
var datums = [];
|
||||
for (var i = 0; i < data.results.length; ++i) {
|
||||
var entity = data.results[i];
|
||||
if ($scope.filter) {
|
||||
var allowed = $scope.filter;
|
||||
var found = 'user';
|
||||
if (entity.kind == 'user') {
|
||||
found = entity.is_robot ? 'robot' : 'user';
|
||||
} else if (entity.kind == 'team') {
|
||||
found = 'team';
|
||||
}
|
||||
if (allowed.indexOf(found)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
datums.push({
|
||||
'value': entity.name,
|
||||
'tokens': [entity.name],
|
||||
|
@ -2849,7 +3158,7 @@ quayApp.directive('entitySearch', function () {
|
|||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.setEntityInternal(datum.entity);
|
||||
$scope.setEntityInternal(datum.entity, true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2861,6 +3170,16 @@ quayApp.directive('entitySearch', function () {
|
|||
$scope.$watch('inputTitle', function(title) {
|
||||
input.setAttribute('placeholder', title);
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if ($scope.currentEntityInternal != entity) {
|
||||
if (entity) {
|
||||
$scope.setEntityInternal(entity, false);
|
||||
} else {
|
||||
$scope.clearEntityInternal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -3072,7 +3391,7 @@ quayApp.directive('planManager', function () {
|
|||
}
|
||||
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new RepositoryUsageChart();
|
||||
$scope.chart = new UsageChart();
|
||||
$scope.chart.draw('repository-usage-chart');
|
||||
}
|
||||
|
||||
|
@ -3398,6 +3717,145 @@ quayApp.directive('dropdownSelectMenu', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('setupTriggerDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger',
|
||||
'counter': '=counter',
|
||||
'canceled': '&canceled',
|
||||
'activated': '&activated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService) {
|
||||
$scope.show = function() {
|
||||
$scope.pullEntity = null;
|
||||
$scope.publicPull = true;
|
||||
$scope.showPullRequirements = false;
|
||||
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.setPublicPull = function(value) {
|
||||
$scope.publicPull = value;
|
||||
};
|
||||
|
||||
$scope.checkAnalyze = function(isValid) {
|
||||
if (!isValid) {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
$scope.showPullRequirements = false;
|
||||
$scope.checkingPullRequirements = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = true;
|
||||
$scope.showPullRequirements = true;
|
||||
$scope.pullRequirements = null;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger.config
|
||||
};
|
||||
|
||||
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
|
||||
if (resp['status'] == 'publicbase') {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
} else if (resp['namespace']) {
|
||||
$scope.publicPull = false;
|
||||
|
||||
if (resp['robots'] && resp['robots'].length > 0) {
|
||||
$scope.pullEntity = resp['robots'][0];
|
||||
} else {
|
||||
$scope.pullEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = false;
|
||||
}, function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
$scope.checkingPullRequirements = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.activate = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger['config']
|
||||
};
|
||||
|
||||
if ($scope.pullEntity) {
|
||||
data['pull_robot'] = $scope.pullEntity['name'];
|
||||
}
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
if ($scope.counter && $scope.trigger && $scope.repository) {
|
||||
$scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('trigger', check);
|
||||
$scope.$watch('counter', check);
|
||||
$scope.$watch('repository', check);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
|
||||
quayApp.directive('triggerSetupGithub', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -3407,15 +3865,18 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger'
|
||||
'trigger': '=trigger',
|
||||
'analyze': '&analyze'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.analyzeCounter = 0;
|
||||
$scope.setupReady = false;
|
||||
$scope.loading = true;
|
||||
|
||||
$scope.handleLocationInput = function(location) {
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
};
|
||||
|
||||
$scope.handleLocationSelected = function(datum) {
|
||||
|
@ -3426,6 +3887,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.currentLocation = location;
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': true});
|
||||
};
|
||||
|
||||
$scope.selectRepo = function(repo, org) {
|
||||
|
@ -3464,6 +3926,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3476,12 +3939,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
} else {
|
||||
$scope.currentLocation = null;
|
||||
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
|
||||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -3526,7 +3991,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
});
|
||||
};
|
||||
|
||||
loadSources();
|
||||
var check = function() {
|
||||
if ($scope.repository && $scope.trigger) {
|
||||
loadSources();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('repository', check);
|
||||
$scope.$watch('trigger', check);
|
||||
|
||||
$scope.$watch('currentRepo', function(repo) {
|
||||
$scope.selectRepoInternal(repo);
|
||||
|
@ -3572,7 +4044,7 @@ quayApp.directive('dockerfileCommand', function () {
|
|||
scope: {
|
||||
'command': '=command'
|
||||
},
|
||||
controller: function($scope, $element, $sanitize) {
|
||||
controller: function($scope, $element, $sanitize, Config) {
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
|
@ -3587,6 +4059,8 @@ quayApp.directive('dockerfileCommand', function () {
|
|||
}
|
||||
};
|
||||
|
||||
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': function(title) {
|
||||
var pieces = title.split('/');
|
||||
|
@ -4259,6 +4733,17 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
});
|
||||
};
|
||||
|
||||
$rootScope.$watch('description', function(description) {
|
||||
if (!description) {
|
||||
description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||
}
|
||||
|
||||
// Note: We set the content of the description tag manually here rather than using Angular binding
|
||||
// because we need the <meta> tag to have a default description that is not of the form "{{ description }}",
|
||||
// we read by tools that do not properly invoke the Angular code.
|
||||
$('#descriptionTag').attr('content', description);
|
||||
});
|
||||
|
||||
$rootScope.$on('$routeUpdate', function(){
|
||||
if ($location.search()['tab']) {
|
||||
changeTab($location.search()['tab']);
|
||||
|
@ -4275,7 +4760,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
if (current.$$route.description) {
|
||||
$rootScope.description = current.$$route.description;
|
||||
} else {
|
||||
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||
$rootScope.description = '';
|
||||
}
|
||||
|
||||
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -945,9 +958,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
|
||||
var data = {
|
||||
'file_id': build['resource_key'],
|
||||
'subdirectory': subdirectory
|
||||
'subdirectory': subdirectory,
|
||||
};
|
||||
|
||||
if (build['pull_robot']) {
|
||||
data['pull_robot'] = build['pull_robot']['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
@ -1073,6 +1090,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.
|
||||
|
@ -1089,6 +1107,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;
|
||||
});
|
||||
|
@ -1131,7 +1161,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
fetchRepository();
|
||||
}
|
||||
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location) {
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
|
@ -1144,15 +1174,17 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
|
||||
$scope.showTriggerSetupCounter = 0;
|
||||
|
||||
$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':
|
||||
|
@ -1433,48 +1465,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$scope.setupTrigger = function(trigger) {
|
||||
$scope.triggerSetupReady = false;
|
||||
$scope.currentSetupTrigger = trigger;
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
$scope.showTriggerSetupCounter++;
|
||||
};
|
||||
|
||||
$scope.finishSetupTrigger = function(trigger) {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.currentSetupTrigger = null;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
}, function(resp) {
|
||||
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
if (!$scope.currentSetupTrigger) { return; }
|
||||
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.deleteTrigger($scope.currentSetupTrigger);
|
||||
$scope.cancelSetupTrigger = function(trigger) {
|
||||
if ($scope.currentSetupTrigger != trigger) { return; }
|
||||
|
||||
$scope.currentSetupTrigger = null;
|
||||
$scope.deleteTrigger(trigger);
|
||||
};
|
||||
|
||||
$scope.startTrigger = function(trigger) {
|
||||
|
@ -1569,12 +1568,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
}
|
||||
|
||||
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
|
||||
$routeParams, $http) {
|
||||
$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++) {
|
||||
|
@ -1602,8 +1605,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.githubClientId = KeyService.githubClientId;
|
||||
$scope.authorizedApps = null;
|
||||
|
||||
$('.form-change').popover();
|
||||
|
||||
$scope.logsShown = 0;
|
||||
$scope.invoicesShown = 0;
|
||||
|
||||
|
@ -1652,13 +1653,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;
|
||||
};
|
||||
|
@ -1673,7 +1676,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) {
|
||||
|
@ -1691,7 +1694,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
};
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
$('#changeEmailForm').popover('hide');
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
|
||||
$scope.updatingUser = true;
|
||||
$scope.changeEmailSent = false;
|
||||
|
||||
|
@ -1706,16 +1710,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.changeEmailForm.$setPristine();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
|
||||
$scope.changeEmailError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changeEmailForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.changePassword = function() {
|
||||
$('#changePasswordForm').popover('hide');
|
||||
UIService.hidePopover('#changePasswordForm');
|
||||
|
||||
$scope.updatingUser = true;
|
||||
$scope.changePasswordSuccess = false;
|
||||
|
||||
|
@ -1733,11 +1734,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
UserService.load();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
|
||||
$scope.changePasswordError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changePasswordForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changePasswordForm', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -1874,7 +1871,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;
|
||||
|
@ -1996,13 +1993,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;
|
||||
}
|
||||
|
@ -2122,18 +2125,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
|||
loadOrganization();
|
||||
}
|
||||
|
||||
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) {
|
||||
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;
|
||||
|
@ -2161,10 +2166,12 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
|||
};
|
||||
|
||||
$scope.$watch('organizationEmail', function(e) {
|
||||
$('#changeEmailForm').popover('hide');
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
});
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
|
||||
$scope.changingOrganization = true;
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
|
@ -2180,10 +2187,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
|||
$scope.organization = org;
|
||||
}, function(result) {
|
||||
$scope.changingOrganization = false;
|
||||
$scope.changeEmailError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changeEmailForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2316,30 +2320,39 @@ 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.currentPlan = null;
|
||||
if (requested) {
|
||||
PlanService.getPlan(requested, function(plan) {
|
||||
$scope.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) {
|
||||
|
@ -2371,7 +2384,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.currentPlan.price == 0) {
|
||||
showOrg();
|
||||
return;
|
||||
}
|
||||
|
@ -2564,4 +2577,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();
|
||||
}
|
|
@ -230,7 +230,17 @@ ImageHistoryTree.prototype.draw = function(container) {
|
|||
if (d.image.command && d.image.command.length) {
|
||||
html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>';
|
||||
}
|
||||
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
||||
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
||||
|
||||
var tags = d.tags || [];
|
||||
html += '<span class="tooltip-tags tags">';
|
||||
for (var i = 0; i < tags.length; ++i) {
|
||||
var tag = tags[i];
|
||||
var kind = 'default';
|
||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
|
||||
}
|
||||
html += '</span>';
|
||||
|
||||
return html;
|
||||
})
|
||||
|
||||
|
@ -338,6 +348,23 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Expands the given collapsed node in the tree.
|
||||
*/
|
||||
ImageHistoryTree.prototype.expandCollapsed_ = function(imageNode) {
|
||||
var index = imageNode.parent.children.indexOf(imageNode);
|
||||
if (index < 0 || imageNode.encountered.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: we start at 1 since the 0th encountered node is the parent.
|
||||
imageNode.parent.children.splice(index, 1, imageNode.encountered[1]);
|
||||
this.maxHeight_ = this.determineMaximumHeight_(this.root_);
|
||||
this.update_(this.root_);
|
||||
this.updateDimensions_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Builds the root node for the tree.
|
||||
*/
|
||||
|
@ -640,7 +667,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
||||
.text(function(d) { return d.name; })
|
||||
.on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
|
||||
.on("click", function(d) {
|
||||
if (d.image) { that.changeImage_(d.image.id); }
|
||||
if (d.collapsed) { that.expandCollapsed_(d); }
|
||||
})
|
||||
.on('mouseover', tip.show)
|
||||
.on('mouseout', tip.hide)
|
||||
.on("contextmenu", function(d, e) {
|
||||
|
@ -729,7 +759,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
if (tag == currentTag) {
|
||||
kind = 'success';
|
||||
}
|
||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"" style="max-width: ' + DEPTH_HEIGHT + 'px">' + 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;
|
||||
|
@ -1350,7 +1380,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;
|
||||
|
@ -1360,7 +1390,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;
|
||||
|
@ -1371,7 +1401,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; }
|
||||
|
||||
|
@ -1430,7 +1460,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;
|
||||
|
@ -1707,7 +1737,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate)
|
|||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
nv.utils.windoweResize(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) {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<dd am-time-ago="parseDate(image.value.created)"></dd>
|
||||
<dt>Compressed Image Size</dt>
|
||||
<dd><span class="context-tooltip"
|
||||
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
|
||||
</dd>
|
||||
|
||||
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
||||
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
||||
<span title="{{change.file}}">
|
||||
<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>
|
||||
|
|
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>
|
|
@ -1,150 +1,3 @@
|
|||
<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 quay-include="{'Features.BILLING': 'landing-normal.html', '!Features.BILLING': 'landing-login.html'}" onload="chromify()">
|
||||
<span class="quay-spinner"></span>
|
||||
</div>
|
|
@ -106,7 +106,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Client Secret: <i class="fa fa-lock fa-lg" title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
|
||||
<td>Client Secret: <i class="fa fa-lock fa-lg" data-title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
|
||||
<td>
|
||||
{{ application.client_secret }}
|
||||
</td>
|
||||
|
|
|
@ -66,13 +66,14 @@
|
|||
</div>
|
||||
|
||||
<!-- Plans Table -->
|
||||
<div class="form-group nested plan-group">
|
||||
<div class="form-group nested plan-group" quay-require="['BILLING']">
|
||||
<strong>Choose your organization's plan</strong>
|
||||
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
|
||||
<div class="plans-table" plans="plans" current-plan="holder.currentPlan"></div>
|
||||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
|
||||
<button class="btn btn-large btn-success" type="submit"
|
||||
ng-disabled="newOrgForm.$invalid || (Features.BILLING && !holder.currentPlan)"
|
||||
analytics-on analytics-event="create_organization">
|
||||
Create Organization
|
||||
</button>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<div class="section">
|
||||
<div class="repo-option">
|
||||
<input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1">
|
||||
<i class="fa fa-unlock fa-large" title="Public Repository"></i>
|
||||
<i class="fa fa-unlock fa-large" data-title="Public Repository"></i>
|
||||
|
||||
<div class="option-description">
|
||||
<label for="publicrepo"><strong>Public</strong></label>
|
||||
|
@ -60,7 +60,7 @@
|
|||
</div>
|
||||
<div class="repo-option">
|
||||
<input type="radio" id="privaterepo" name="publicorprivate" ng-model="repo.is_public" value="0">
|
||||
<i class="fa fa-lock fa-large" title="Private Repository"></i>
|
||||
<i class="fa fa-lock fa-large" data-title="Private Repository"></i>
|
||||
|
||||
<div class="option-description">
|
||||
<label for="privaterepo"><strong>Private</strong></label>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<span ng-if="isUserNamespace">under your personal namespace</span>
|
||||
<span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to
|
||||
<b style="border-bottom: 1px dotted black;" data-html="true"
|
||||
title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
||||
data-title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
||||
{{ planRequired.title }}
|
||||
</b>.
|
||||
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||
|
@ -118,11 +118,11 @@
|
|||
<label for="initDockerfile">Initialize from a <b>Dockerfile</b></label>
|
||||
</div>
|
||||
|
||||
<!-- Zip file -->
|
||||
<!-- Zip/TarGz file -->
|
||||
<div class="repo-option">
|
||||
<input type="radio" id="initZipfile" name="initialize" ng-model="repo.initialize" value="zipfile">
|
||||
<i class="fa fa-archive fa-lg" style="padding: 6px; padding-left: 10px; padding-right: 8px;"></i>
|
||||
<label for="initZipfile">Initialize from a ZIP file (containing a Dockerfile and other supporting files)</label>
|
||||
<label for="initZipfile">Initialize from a <b>Dockerfile</b> inside a <code>.zip</code> or <code>.tar.gz</code> archive</label>
|
||||
</div>
|
||||
|
||||
<!-- Github -->
|
||||
|
@ -140,7 +140,7 @@
|
|||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="section">
|
||||
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">ZIP file</span></div>
|
||||
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">Archive</span></div>
|
||||
<div style="padding-top: 20px;">
|
||||
<div class="initialize-repo">
|
||||
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
|
||||
|
|
|
@ -6,15 +6,23 @@
|
|||
<!-- Side tabs -->
|
||||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a></li>
|
||||
<li class="active" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a>
|
||||
</li>
|
||||
<li quay-classes="{'!Features.BILLING': 'active'}">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a>
|
||||
</li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a>
|
||||
</li>
|
||||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -22,12 +30,12 @@
|
|||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<!-- Plans tab -->
|
||||
<div id="plan" class="tab-pane active">
|
||||
<div id="plan" class="tab-pane active" quay-require="['BILLING']">
|
||||
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Organization settings tab -->
|
||||
<div id="settings" class="tab-pane">
|
||||
<div id="settings" class="tab-pane" quay-classes="{'!Features.BILLING': 'active'}">
|
||||
<div class="quay-spinner" ng-show="changingOrganization"></div>
|
||||
|
||||
<div class="panel" ng-show="!changingOrganization">
|
||||
|
@ -67,12 +75,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Billing Options tab -->
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
|
||||
<div class="billing-options" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing History tab -->
|
||||
<div id="billing" class="tab-pane">
|
||||
<div id="billing" class="tab-pane" quay-require="['BILLING']">
|
||||
<div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
|
||||
</div>
|
||||
|
||||
|
@ -106,7 +114,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" title="Member Usage Logs" bs-tooltip="tooltip.title">
|
||||
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" data-title="Member Usage Logs" bs-tooltip="tooltip.title">
|
||||
<i class="fa fa-book"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="row hidden-xs">
|
||||
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
|
||||
Team Permissions
|
||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
|
||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
|
||||
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar-right">
|
||||
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-plus"></i>
|
||||
Create New Organization
|
||||
</button>
|
||||
</a>
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fa fa-caret-square-o-right"></i>
|
||||
Convert account
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-list.png" data-title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">A central collection of repositories</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -57,7 +57,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
||||
<div class="col-md-5 col-md-pull-7">
|
||||
<div class="tour-section-title">Organization settings at a glance</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -73,7 +73,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-logs.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-logs.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">Logging for comprehensive analysis</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -94,7 +94,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" data-title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-5 col-md-pull-7">
|
||||
<div class="tour-section-title">Teams simplify access controls</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -115,7 +115,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" data-title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">Fine-grained control of sharing</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -133,13 +133,13 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar-right button-bar-bottom">
|
||||
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-plus"></i>
|
||||
Create New Organization
|
||||
</button>
|
||||
</a>
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fa fa-caret-square-o-right"></i>
|
||||
Convert account
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have unlimited public repositories">
|
||||
data-title="All plans have unlimited public repositories">
|
||||
<span class="hidden-sm-inline">Public Repositories</span>
|
||||
<span class="visible-sm-inline">Public Repos</span>
|
||||
</span>
|
||||
|
@ -15,49 +15,49 @@
|
|||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="SSL encryption is enabled end-to-end for all operations">
|
||||
data-title="SSL encryption is enabled end-to-end for all operations">
|
||||
SSL Encryption
|
||||
</span>
|
||||
<i class="fa fa-lock visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||
data-title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||
Robot accounts
|
||||
</span>
|
||||
<i class="fa fa-wrench visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Repository images can be built directly from Dockerfiles">
|
||||
data-title="Repository images can be built directly from Dockerfiles">
|
||||
Dockerfile Build
|
||||
</span>
|
||||
<i class="fa fa-upload visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
Teams
|
||||
</span>
|
||||
<i class="fa fa-group visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||
data-title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||
Logging
|
||||
</span>
|
||||
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Administrators can view and download the full invoice history for their organization">
|
||||
data-title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have a 14-day free trial">
|
||||
data-title="All plans have a 14-day free trial">
|
||||
<span class="hidden-sm-inline">14-Day Free Trial</span>
|
||||
<span class="visible-sm-inline">14-Day Trial</span>
|
||||
</span>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
<!-- Status Image -->
|
||||
<a ng-href="/repository/{{ repo.namespace }}/{{ repo.name }}" ng-if="repo && repo.name">
|
||||
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" title="Docker Repository on Quay.io">
|
||||
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" data-title="Docker Repository on Quay.io">
|
||||
</a>
|
||||
|
||||
<!-- Embed formats -->
|
||||
|
@ -270,10 +270,16 @@
|
|||
Setting up trigger
|
||||
</div>
|
||||
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div>
|
||||
<div class="trigger-pull-credentials" ng-if="trigger.is_active && trigger.pull_robot">
|
||||
<span class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</span>
|
||||
<span class="entity-reference" entity="trigger.pull_robot"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="white-space: nowrap;">
|
||||
<div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build History" bs-tooltip="tooltip.title" data-container="body"
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Build History" bs-tooltip="tooltip.title" data-container="body"
|
||||
ng-click="loadTriggerBuildHistory(trigger)">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<b class="caret"></b>
|
||||
|
@ -291,7 +297,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dropdown" style="display: inline-block">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
|
||||
<i class="fa fa-cog"></i>
|
||||
<b class="caret"></b>
|
||||
</button>
|
||||
|
@ -371,36 +377,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Auth dialog -->
|
||||
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
||||
shown="!!shownToken" counter="shownTokenCounter">
|
||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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="trigger-description-element" ng-switch on="currentSetupTrigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!currentSetupTrigger.$ready" ng-click="finishSetupTrigger(currentSetupTrigger)">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Setup trigger dialog-->
|
||||
<div class="setup-trigger-dialog" repository="repo"
|
||||
trigger="currentSetupTrigger"
|
||||
canceled="cancelSetupTrigger(trigger)"
|
||||
counter="showTriggerSetupCounter"></div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotchangeModal">
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<i class="fa fa-archive"></i>
|
||||
<a href="/repository/{{ repo.namespace }}/{{ repo.name }}/build/{{ currentBuild.id }}/buildpack"
|
||||
style="display: inline-block; margin-left: 6px" bs-tooltip="tooltip.title"
|
||||
title="View the uploaded build package for this build">Build Package</a>
|
||||
data-title="View the uploaded build package for this build">Build Package</a>
|
||||
</span>
|
||||
</div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="button-bar-right">
|
||||
<a href="/new/">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-upload user-tool" title="Create new repository"></i>
|
||||
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
|
||||
Create Repository
|
||||
</button>
|
||||
</a>
|
||||
|
@ -60,11 +60,11 @@
|
|||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
<div class="page-controls">
|
||||
<button class="btn btn-default" title="Previous Page" bs-tooltip="title" ng-show="page > 1"
|
||||
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
|
||||
ng-click="movePublicPage(-1)">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-default" title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
|
||||
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
|
||||
ng-click="movePublicPage(1)">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="container signin-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<div class="user-setup"></div>
|
||||
<div class="user-setup" redirect-url="'/'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
149
static/partials/super-user.html
Normal file
149
static/partials/super-user.html
Normal file
|
@ -0,0 +1,149 @@
|
|||
<div class="container" quay-show="Features.SUPER_USERS">
|
||||
<div class="alert alert-info">
|
||||
This panel provides administrator access to <strong>super users of this installation of Quay.io</strong>. Super users can be managed in the configuration for this installation.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Side tabs -->
|
||||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<!-- License tab -->
|
||||
<div id="license" class="tab-pane active">
|
||||
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
|
||||
<!-- Chart -->
|
||||
<div>
|
||||
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
|
||||
<span class="usage-caption" ng-show="chart">Seat Usage</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Users tab -->
|
||||
<div id="users" class="tab-pane">
|
||||
<div class="quay-spinner" ng-show="!users"></div>
|
||||
<div class="alert alert-error" ng-show="usersError">
|
||||
{{ usersError }}
|
||||
</div>
|
||||
<div ng-show="users">
|
||||
<div class="side-controls">
|
||||
<div class="result-count">
|
||||
Showing {{(users | filter:search | limitTo:100).length}} of
|
||||
{{(users | filter:search).length}} matching users
|
||||
</div>
|
||||
<div class="filter-input">
|
||||
<input id="log-filter" class="form-control" placeholder="Filter Users" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Username</th>
|
||||
<th>E-mail address</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
|
||||
class="user-row"
|
||||
ng-class="current_user.super_user ? 'super-user' : ''">
|
||||
<td>
|
||||
<i class="fa fa-user" style="margin-right: 6px"></i>
|
||||
{{ current_user.username }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
|
||||
</td>
|
||||
<td class="user-class">
|
||||
<span ng-if="current_user.super_user">Super user</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown" ng-if="user.username != current_user.username && !user.super_user">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
|
||||
<i class="fa fa-key"></i> Change Password
|
||||
</a>
|
||||
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
|
||||
<i class="fa fa-times"></i> Delete User
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmDeleteUserModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Delete User?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger">
|
||||
This operation <strong>cannot be undone</strong> and will <strong>delete any repositories owned by the user</strong>.
|
||||
</div>
|
||||
Are you <strong>sure</strong> you want to delete user <strong>{{ userToDelete.username }}</strong>?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteUser(userToDelete)">Delete User</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="changePasswordModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Change User Password</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
The user will no longer be able to access Quay.io with their current password
|
||||
</div>
|
||||
|
||||
<form class="form-change" id="changePasswordForm" name="changePasswordForm" data-trigger="manual">
|
||||
<input type="password" class="form-control" placeholder="User's new password" ng-model="userToChange.password" required ng-pattern="/^.{8,}$/">
|
||||
<input type="password" class="form-control" placeholder="Verify the new password" ng-model="userToChange.repeatPassword"
|
||||
match="userToChange.password" required ng-pattern="/^.{8,}$/">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="changeUserPassword(userToChange)"
|
||||
ng-disabled="changePasswordForm.$invalid">Change User Password</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue