Merge branch master into bees
This commit is contained in:
commit
1d8ec59362
164 changed files with 6048 additions and 1911 deletions
|
@ -1,11 +1,11 @@
|
||||||
conf/stack
|
conf/stack
|
||||||
screenshots
|
screenshots
|
||||||
|
tools
|
||||||
test/data/registry
|
test/data/registry
|
||||||
venv
|
venv
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
Bobfile
|
Bobfile
|
||||||
README.md
|
README.md
|
||||||
license.py
|
|
||||||
requirements-nover.txt
|
requirements-nover.txt
|
||||||
run-local.sh
|
run-local.sh
|
|
@ -1,13 +1,13 @@
|
||||||
FROM phusion/baseimage:0.9.11
|
FROM phusion/baseimage:0.9.13
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
ENV HOME /root
|
ENV HOME /root
|
||||||
|
|
||||||
# Install the dependencies.
|
# Install the dependencies.
|
||||||
RUN apt-get update # 06AUG2014
|
RUN apt-get update # 10SEP2014
|
||||||
|
|
||||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev
|
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev libpq-dev
|
||||||
|
|
||||||
# Build the python dependencies
|
# Build the python dependencies
|
||||||
ADD requirements.txt requirements.txt
|
ADD requirements.txt requirements.txt
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
FROM phusion/baseimage:0.9.11
|
FROM phusion/baseimage:0.9.13
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
ENV HOME /root
|
ENV HOME /root
|
||||||
|
|
||||||
# Install the dependencies.
|
# Install the dependencies.
|
||||||
RUN apt-get update # 06AUG2014
|
RUN apt-get update # 10SEP2014
|
||||||
|
|
||||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev
|
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev libpq-dev
|
||||||
|
|
||||||
# Build the python dependencies
|
# Build the python dependencies
|
||||||
ADD requirements.txt requirements.txt
|
ADD requirements.txt requirements.txt
|
||||||
|
@ -30,6 +30,7 @@ RUN cd grunt && npm install
|
||||||
RUN cd grunt && grunt
|
RUN cd grunt && grunt
|
||||||
|
|
||||||
ADD conf/init/svlogd_config /svlogd_config
|
ADD conf/init/svlogd_config /svlogd_config
|
||||||
|
ADD conf/init/doupdatelimits.sh /etc/my_init.d/
|
||||||
ADD conf/init/preplogsdir.sh /etc/my_init.d/
|
ADD conf/init/preplogsdir.sh /etc/my_init.d/
|
||||||
ADD conf/init/runmigration.sh /etc/my_init.d/
|
ADD conf/init/runmigration.sh /etc/my_init.d/
|
||||||
|
|
||||||
|
@ -37,9 +38,7 @@ ADD conf/init/gunicorn /etc/service/gunicorn
|
||||||
ADD conf/init/nginx /etc/service/nginx
|
ADD conf/init/nginx /etc/service/nginx
|
||||||
ADD conf/init/diffsworker /etc/service/diffsworker
|
ADD conf/init/diffsworker /etc/service/diffsworker
|
||||||
ADD conf/init/notificationworker /etc/service/notificationworker
|
ADD conf/init/notificationworker /etc/service/notificationworker
|
||||||
|
ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
|
||||||
# TODO: Remove this after the prod CL push
|
|
||||||
ADD conf/init/webhookworker /etc/service/webhookworker
|
|
||||||
|
|
||||||
# Download any external libs.
|
# Download any external libs.
|
||||||
RUN mkdir static/fonts static/ldn
|
RUN mkdir static/fonts static/ldn
|
||||||
|
@ -48,7 +47,7 @@ RUN venv/bin/python -m external_libraries
|
||||||
# Run the tests
|
# Run the tests
|
||||||
RUN TEST=true venv/bin/python -m unittest discover
|
RUN TEST=true venv/bin/python -m unittest discover
|
||||||
|
|
||||||
VOLUME ["/conf/stack", "/var/log", "/datastorage"]
|
VOLUME ["/conf/stack", "/var/log", "/datastorage", "/tmp"]
|
||||||
|
|
||||||
EXPOSE 443 80
|
EXPOSE 443 80
|
||||||
|
|
||||||
|
|
56
app.py
56
app.py
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask as BaseFlask, Config as BaseConfig
|
||||||
from flask.ext.principal import Principal
|
from flask.ext.principal import Principal
|
||||||
from flask.ext.login import LoginManager
|
from flask.ext.login import LoginManager
|
||||||
from flask.ext.mail import Mail
|
from flask.ext.mail import Mail
|
||||||
|
@ -19,13 +20,40 @@ from util.exceptionlog import Sentry
|
||||||
from util.queuemetrics import QueueMetrics
|
from util.queuemetrics import QueueMetrics
|
||||||
from data.billing import Billing
|
from data.billing import Billing
|
||||||
from data.buildlogs import BuildLogs
|
from data.buildlogs import BuildLogs
|
||||||
|
from data.archivedlogs import LogArchive
|
||||||
from data.queue import WorkQueue
|
from data.queue import WorkQueue
|
||||||
from data.userevent import UserEventsBuilderModule
|
from data.userevent import UserEventsBuilderModule
|
||||||
from license import load_license
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
OVERRIDE_CONFIG_FILENAME = 'conf/stack/config.py'
|
class Config(BaseConfig):
|
||||||
|
""" Flask config enhanced with a `from_yamlfile` method """
|
||||||
|
|
||||||
|
def from_yamlfile(self, config_file):
|
||||||
|
with open(config_file) as f:
|
||||||
|
c = yaml.load(f)
|
||||||
|
if not c:
|
||||||
|
logger.debug('Empty YAML config file')
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(c, str):
|
||||||
|
raise Exception('Invalid YAML config file: ' + str(c))
|
||||||
|
|
||||||
|
for key in c.iterkeys():
|
||||||
|
if key.isupper():
|
||||||
|
self[key] = c[key]
|
||||||
|
|
||||||
|
class Flask(BaseFlask):
|
||||||
|
""" Extends the Flask class to implement our custom Config class. """
|
||||||
|
|
||||||
|
def make_config(self, instance_relative=False):
|
||||||
|
root_path = self.instance_path if instance_relative else self.root_path
|
||||||
|
return Config(root_path, self.default_config)
|
||||||
|
|
||||||
|
|
||||||
|
OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
|
||||||
|
OVERRIDE_CONFIG_PY_FILENAME = 'conf/stack/config.py'
|
||||||
|
|
||||||
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
|
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
|
||||||
LICENSE_FILENAME = 'conf/stack/license.enc'
|
LICENSE_FILENAME = 'conf/stack/license.enc'
|
||||||
|
|
||||||
|
@ -43,22 +71,17 @@ else:
|
||||||
logger.debug('Loading default config.')
|
logger.debug('Loading default config.')
|
||||||
app.config.from_object(DefaultConfig())
|
app.config.from_object(DefaultConfig())
|
||||||
|
|
||||||
if os.path.exists(OVERRIDE_CONFIG_FILENAME):
|
if os.path.exists(OVERRIDE_CONFIG_PY_FILENAME):
|
||||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME)
|
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_PY_FILENAME)
|
||||||
app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME)
|
app.config.from_pyfile(OVERRIDE_CONFIG_PY_FILENAME)
|
||||||
|
|
||||||
|
if os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME):
|
||||||
|
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_YAML_FILENAME)
|
||||||
|
app.config.from_yamlfile(OVERRIDE_CONFIG_YAML_FILENAME)
|
||||||
|
|
||||||
environ_config = json.loads(os.environ.get(OVERRIDE_CONFIG_KEY, '{}'))
|
environ_config = json.loads(os.environ.get(OVERRIDE_CONFIG_KEY, '{}'))
|
||||||
app.config.update(environ_config)
|
app.config.update(environ_config)
|
||||||
|
|
||||||
logger.debug('Applying license config from: %s', LICENSE_FILENAME)
|
|
||||||
try:
|
|
||||||
app.config.update(load_license(LICENSE_FILENAME))
|
|
||||||
except IOError:
|
|
||||||
raise RuntimeError('License file %s not found; please check your configuration' % LICENSE_FILENAME)
|
|
||||||
|
|
||||||
if app.config.get('LICENSE_EXPIRATION', datetime.min) < datetime.utcnow():
|
|
||||||
raise RuntimeError('License has expired, please contact support@quay.io')
|
|
||||||
|
|
||||||
features.import_features(app.config)
|
features.import_features(app.config)
|
||||||
|
|
||||||
Principal(app, use_sessions=False)
|
Principal(app, use_sessions=False)
|
||||||
|
@ -66,7 +89,8 @@ Principal(app, use_sessions=False)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
storage = Storage(app)
|
storage = Storage(app)
|
||||||
userfiles = Userfiles(app)
|
userfiles = Userfiles(app, storage)
|
||||||
|
log_archive = LogArchive(app, storage)
|
||||||
analytics = Analytics(app)
|
analytics = Analytics(app)
|
||||||
billing = Billing(app)
|
billing = Billing(app)
|
||||||
sentry = Sentry(app)
|
sentry = Sentry(app)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from peewee import Proxy
|
||||||
from app import app as application
|
from app import app as application
|
||||||
from flask import request, Request
|
from flask import request, Request
|
||||||
from util.names import urn_generator
|
from util.names import urn_generator
|
||||||
from data.model import db as model_db, read_slave
|
from data.database import db as model_db, read_slave
|
||||||
|
|
||||||
# Turn off debug logging for boto
|
# Turn off debug logging for boto
|
||||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||||
|
|
20
auth/auth.py
20
auth/auth.py
|
@ -25,7 +25,7 @@ def _load_user_from_cookie():
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
logger.debug('Loading user from cookie: %s', current_user.get_id())
|
logger.debug('Loading user from cookie: %s', current_user.get_id())
|
||||||
set_authenticated_user_deferred(current_user.get_id())
|
set_authenticated_user_deferred(current_user.get_id())
|
||||||
loaded = QuayDeferredPermissionUser(current_user.get_id(), 'username', {scopes.DIRECT_LOGIN})
|
loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=loaded)
|
identity_changed.send(app, identity=loaded)
|
||||||
return current_user.db_user()
|
return current_user.db_user()
|
||||||
return None
|
return None
|
||||||
|
@ -58,12 +58,10 @@ def _validate_and_apply_oauth_token(token):
|
||||||
set_authenticated_user(validated.authorized_user)
|
set_authenticated_user(validated.authorized_user)
|
||||||
set_validated_oauth_token(validated)
|
set_validated_oauth_token(validated)
|
||||||
|
|
||||||
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
new_identity = QuayDeferredPermissionUser(validated.authorized_user.id, 'user_db_id', scope_set)
|
||||||
scope_set)
|
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def process_basic_auth(auth):
|
def process_basic_auth(auth):
|
||||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||||
if normalized[0].lower() != 'basic' or len(normalized) != 2:
|
if normalized[0].lower() != 'basic' or len(normalized) != 2:
|
||||||
|
@ -100,8 +98,7 @@ def process_basic_auth(auth):
|
||||||
logger.debug('Successfully validated robot: %s' % credentials[0])
|
logger.debug('Successfully validated robot: %s' % credentials[0])
|
||||||
set_authenticated_user(robot)
|
set_authenticated_user(robot)
|
||||||
|
|
||||||
deferred_robot = QuayDeferredPermissionUser(robot.username, 'username',
|
deferred_robot = QuayDeferredPermissionUser(robot.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
{scopes.DIRECT_LOGIN})
|
|
||||||
identity_changed.send(app, identity=deferred_robot)
|
identity_changed.send(app, identity=deferred_robot)
|
||||||
return
|
return
|
||||||
except model.InvalidRobotException:
|
except model.InvalidRobotException:
|
||||||
|
@ -114,7 +111,7 @@ def process_basic_auth(auth):
|
||||||
logger.debug('Successfully validated user: %s' % authenticated.username)
|
logger.debug('Successfully validated user: %s' % authenticated.username)
|
||||||
set_authenticated_user(authenticated)
|
set_authenticated_user(authenticated)
|
||||||
|
|
||||||
new_identity = QuayDeferredPermissionUser(authenticated.username, 'username',
|
new_identity = QuayDeferredPermissionUser(authenticated.id, 'user_db_id',
|
||||||
{scopes.DIRECT_LOGIN})
|
{scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
return
|
return
|
||||||
|
@ -135,8 +132,15 @@ def process_token(auth):
|
||||||
logger.warning('Invalid token format: %s' % auth)
|
logger.warning('Invalid token format: %s' % auth)
|
||||||
abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth)
|
abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth)
|
||||||
|
|
||||||
token_vals = {val[0]: val[1] for val in
|
def safe_get(lst, index, default_value):
|
||||||
|
try:
|
||||||
|
return lst[index]
|
||||||
|
except IndexError:
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
token_vals = {val[0]: safe_get(val, 1, '') for val in
|
||||||
(detail.split('=') for detail in token_details)}
|
(detail.split('=') for detail in token_details)}
|
||||||
|
|
||||||
if 'signature' not in token_vals:
|
if 'signature' not in token_vals:
|
||||||
logger.warning('Token does not contain signature: %s' % auth)
|
logger.warning('Token does not contain signature: %s' % auth)
|
||||||
abort(401, message='Token does not contain a valid signature: %(auth)s',
|
abort(401, message='Token does not contain a valid signature: %(auth)s',
|
||||||
|
|
|
@ -10,13 +10,13 @@ logger = logging.getLogger(__name__)
|
||||||
def get_authenticated_user():
|
def get_authenticated_user():
|
||||||
user = getattr(_request_ctx_stack.top, 'authenticated_user', None)
|
user = getattr(_request_ctx_stack.top, 'authenticated_user', None)
|
||||||
if not user:
|
if not user:
|
||||||
username = getattr(_request_ctx_stack.top, 'authenticated_username', None)
|
db_id = getattr(_request_ctx_stack.top, 'authenticated_db_id', None)
|
||||||
if not username:
|
if not db_id:
|
||||||
logger.debug('No authenticated user or deferred username.')
|
logger.debug('No authenticated user or deferred database id.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug('Loading deferred authenticated user.')
|
logger.debug('Loading deferred authenticated user.')
|
||||||
loaded = model.get_user(username)
|
loaded = model.get_user_by_id(db_id)
|
||||||
set_authenticated_user(loaded)
|
set_authenticated_user(loaded)
|
||||||
user = loaded
|
user = loaded
|
||||||
|
|
||||||
|
@ -30,10 +30,10 @@ def set_authenticated_user(user_or_robot):
|
||||||
ctx.authenticated_user = user_or_robot
|
ctx.authenticated_user = user_or_robot
|
||||||
|
|
||||||
|
|
||||||
def set_authenticated_user_deferred(username_or_robotname):
|
def set_authenticated_user_deferred(user_or_robot_db_id):
|
||||||
logger.debug('Deferring loading of authenticated user object: %s', username_or_robotname)
|
logger.debug('Deferring loading of authenticated user object: %s', user_or_robot_db_id)
|
||||||
ctx = _request_ctx_stack.top
|
ctx = _request_ctx_stack.top
|
||||||
ctx.authenticated_username = username_or_robotname
|
ctx.authenticated_db_id = user_or_robot_db_id
|
||||||
|
|
||||||
|
|
||||||
def get_validated_oauth_token():
|
def get_validated_oauth_token():
|
||||||
|
|
|
@ -58,8 +58,8 @@ SCOPE_MAX_USER_ROLES.update({
|
||||||
|
|
||||||
|
|
||||||
class QuayDeferredPermissionUser(Identity):
|
class QuayDeferredPermissionUser(Identity):
|
||||||
def __init__(self, id, auth_type, scopes):
|
def __init__(self, db_id, auth_type, scopes):
|
||||||
super(QuayDeferredPermissionUser, self).__init__(id, auth_type)
|
super(QuayDeferredPermissionUser, self).__init__(db_id, auth_type)
|
||||||
|
|
||||||
self._permissions_loaded = False
|
self._permissions_loaded = False
|
||||||
self._scope_set = scopes
|
self._scope_set = scopes
|
||||||
|
@ -88,7 +88,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
def can(self, permission):
|
def can(self, permission):
|
||||||
if not self._permissions_loaded:
|
if not self._permissions_loaded:
|
||||||
logger.debug('Loading user permissions after deferring.')
|
logger.debug('Loading user permissions after deferring.')
|
||||||
user_object = model.get_user(self.id)
|
user_object = model.get_user_by_id(self.id)
|
||||||
|
|
||||||
# Add the superuser need, if applicable.
|
# Add the superuser need, if applicable.
|
||||||
if (user_object.username is not None and
|
if (user_object.username is not None and
|
||||||
|
@ -112,7 +112,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
|
|
||||||
# Add repository permissions
|
# Add repository permissions
|
||||||
for perm in model.get_all_user_permissions(user_object):
|
for perm in model.get_all_user_permissions(user_object):
|
||||||
repo_grant = _RepositoryNeed(perm.repository.namespace, perm.repository.name,
|
repo_grant = _RepositoryNeed(perm.repository.namespace_user.username, perm.repository.name,
|
||||||
self._repo_role_for_scopes(perm.role.name))
|
self._repo_role_for_scopes(perm.role.name))
|
||||||
logger.debug('User added permission: {0}'.format(repo_grant))
|
logger.debug('User added permission: {0}'.format(repo_grant))
|
||||||
self.provides.add(repo_grant)
|
self.provides.add(repo_grant)
|
||||||
|
@ -230,16 +230,16 @@ def on_identity_loaded(sender, identity):
|
||||||
if isinstance(identity, QuayDeferredPermissionUser):
|
if isinstance(identity, QuayDeferredPermissionUser):
|
||||||
logger.debug('Deferring permissions for user: %s', identity.id)
|
logger.debug('Deferring permissions for user: %s', identity.id)
|
||||||
|
|
||||||
elif identity.auth_type == 'username':
|
elif identity.auth_type == 'user_db_id':
|
||||||
logger.debug('Switching username permission to deferred object: %s', identity.id)
|
logger.debug('Switching username permission to deferred object: %s', identity.id)
|
||||||
switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'username', {scopes.DIRECT_LOGIN})
|
switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=switch_to_deferred)
|
identity_changed.send(app, identity=switch_to_deferred)
|
||||||
|
|
||||||
elif identity.auth_type == 'token':
|
elif identity.auth_type == 'token':
|
||||||
logger.debug('Loading permissions for token: %s', identity.id)
|
logger.debug('Loading permissions for token: %s', identity.id)
|
||||||
token_data = model.load_token_data(identity.id)
|
token_data = model.load_token_data(identity.id)
|
||||||
|
|
||||||
repo_grant = _RepositoryNeed(token_data.repository.namespace,
|
repo_grant = _RepositoryNeed(token_data.repository.namespace_user.username,
|
||||||
token_data.repository.name,
|
token_data.repository.name,
|
||||||
token_data.role.name)
|
token_data.role.name)
|
||||||
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
bind = 'unix:/tmp/gunicorn.sock'
|
bind = 'unix:/tmp/gunicorn.sock'
|
||||||
workers = 8
|
workers = 16
|
||||||
worker_class = 'gevent'
|
worker_class = 'gevent'
|
||||||
timeout = 2000
|
timeout = 2000
|
||||||
logconfig = 'conf/logging.conf'
|
logconfig = 'conf/logging.conf'
|
||||||
|
|
2
conf/init/buildlogsarchiver/log/run
Executable file
2
conf/init/buildlogsarchiver/log/run
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec svlogd /var/log/buildlogsarchiver/
|
8
conf/init/buildlogsarchiver/run
Executable file
8
conf/init/buildlogsarchiver/run
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
echo 'Starting build logs archiver worker'
|
||||||
|
|
||||||
|
cd /
|
||||||
|
venv/bin/python -m workers.buildlogsarchiver 2>&1
|
||||||
|
|
||||||
|
echo 'Diffs worker exited'
|
5
conf/init/doupdatelimits.sh
Executable file
5
conf/init/doupdatelimits.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#! /bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Update the connection limit
|
||||||
|
sysctl -w net.core.somaxconn=1024
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
exec svlogd -t /var/log/webhookworker/
|
|
|
@ -1,8 +0,0 @@
|
||||||
#! /bin/bash
|
|
||||||
|
|
||||||
echo 'Starting webhook worker'
|
|
||||||
|
|
||||||
cd /
|
|
||||||
venv/bin/python -m workers.webhookworker
|
|
||||||
|
|
||||||
echo 'Webhook worker exited'
|
|
|
@ -1,4 +1,4 @@
|
||||||
client_max_body_size 8G;
|
client_max_body_size 20G;
|
||||||
client_body_temp_path /var/log/nginx/client_body 1 2;
|
client_body_temp_path /var/log/nginx/client_body 1 2;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
|
|
37
config.py
37
config.py
|
@ -19,7 +19,7 @@ def build_requests_session():
|
||||||
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
|
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
|
||||||
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
|
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
|
||||||
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE',
|
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE',
|
||||||
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT']
|
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'GOOGLE_LOGIN_CLIENT_ID']
|
||||||
|
|
||||||
|
|
||||||
def getFrontendVisibleConfig(config_dict):
|
def getFrontendVisibleConfig(config_dict):
|
||||||
|
@ -80,19 +80,15 @@ class DefaultConfig(object):
|
||||||
AUTHENTICATION_TYPE = 'Database'
|
AUTHENTICATION_TYPE = 'Database'
|
||||||
|
|
||||||
# Build logs
|
# Build logs
|
||||||
BUILDLOGS_REDIS_HOSTNAME = 'logs.quay.io'
|
BUILDLOGS_REDIS = {'host': 'logs.quay.io'}
|
||||||
BUILDLOGS_OPTIONS = []
|
BUILDLOGS_OPTIONS = []
|
||||||
|
|
||||||
# Real-time user events
|
# Real-time user events
|
||||||
USER_EVENTS_REDIS_HOSTNAME = 'logs.quay.io'
|
USER_EVENTS_REDIS = {'host': 'logs.quay.io'}
|
||||||
|
|
||||||
# Stripe config
|
# Stripe config
|
||||||
BILLING_TYPE = 'FakeStripe'
|
BILLING_TYPE = 'FakeStripe'
|
||||||
|
|
||||||
# Userfiles
|
|
||||||
USERFILES_TYPE = 'LocalUserfiles'
|
|
||||||
USERFILES_PATH = 'test/data/registry/userfiles'
|
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
ANALYTICS_TYPE = 'FakeAnalytics'
|
ANALYTICS_TYPE = 'FakeAnalytics'
|
||||||
|
|
||||||
|
@ -115,6 +111,13 @@ class DefaultConfig(object):
|
||||||
GITHUB_LOGIN_CLIENT_ID = ''
|
GITHUB_LOGIN_CLIENT_ID = ''
|
||||||
GITHUB_LOGIN_CLIENT_SECRET = ''
|
GITHUB_LOGIN_CLIENT_SECRET = ''
|
||||||
|
|
||||||
|
# Google Config.
|
||||||
|
GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
|
||||||
|
GOOGLE_USER_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'
|
||||||
|
|
||||||
|
GOOGLE_LOGIN_CLIENT_ID = ''
|
||||||
|
GOOGLE_LOGIN_CLIENT_SECRET = ''
|
||||||
|
|
||||||
# Requests based HTTP client with a large request pool
|
# Requests based HTTP client with a large request pool
|
||||||
HTTPCLIENT = build_requests_session()
|
HTTPCLIENT = build_requests_session()
|
||||||
|
|
||||||
|
@ -144,6 +147,9 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether GitHub login is supported.
|
# Feature Flag: Whether GitHub login is supported.
|
||||||
FEATURE_GITHUB_LOGIN = False
|
FEATURE_GITHUB_LOGIN = False
|
||||||
|
|
||||||
|
# Feature Flag: Whether Google login is supported.
|
||||||
|
FEATURE_GOOGLE_LOGIN = False
|
||||||
|
|
||||||
# Feature flag, whether to enable olark chat
|
# Feature flag, whether to enable olark chat
|
||||||
FEATURE_OLARK_CHAT = False
|
FEATURE_OLARK_CHAT = False
|
||||||
|
|
||||||
|
@ -153,9 +159,26 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether to support GitHub build triggers.
|
# Feature Flag: Whether to support GitHub build triggers.
|
||||||
FEATURE_GITHUB_BUILD = False
|
FEATURE_GITHUB_BUILD = False
|
||||||
|
|
||||||
|
# Feature Flag: Dockerfile build support.
|
||||||
|
FEATURE_BUILD_SUPPORT = True
|
||||||
|
|
||||||
|
# Feature Flag: Whether emails are enabled.
|
||||||
|
FEATURE_MAILING = True
|
||||||
|
|
||||||
|
# Feature Flag: Whether users can be created (by non-super users).
|
||||||
|
FEATURE_USER_CREATION = True
|
||||||
|
|
||||||
DISTRIBUTED_STORAGE_CONFIG = {
|
DISTRIBUTED_STORAGE_CONFIG = {
|
||||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||||
}
|
}
|
||||||
|
|
||||||
DISTRIBUTED_STORAGE_PREFERENCE = ['local_us']
|
DISTRIBUTED_STORAGE_PREFERENCE = ['local_us']
|
||||||
|
|
||||||
|
# Userfiles
|
||||||
|
USERFILES_LOCATION = 'local_us'
|
||||||
|
USERFILES_PATH = 'userfiles/'
|
||||||
|
|
||||||
|
# Build logs archive
|
||||||
|
LOG_ARCHIVE_LOCATION = 'local_us'
|
||||||
|
LOG_ARCHIVE_PATH = 'logarchive/'
|
||||||
|
|
56
data/archivedlogs.py
Normal file
56
data/archivedlogs.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from gzip import GzipFile
|
||||||
|
from flask import send_file, abort
|
||||||
|
from cStringIO import StringIO
|
||||||
|
|
||||||
|
from data.userfiles import DelegateUserfiles, UserfilesHandlers
|
||||||
|
|
||||||
|
|
||||||
|
JSON_MIMETYPE = 'application/json'
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LogArchiveHandlers(UserfilesHandlers):
|
||||||
|
def get(self, file_id):
|
||||||
|
path = self._files.get_file_id_path(file_id)
|
||||||
|
try:
|
||||||
|
with self._storage.stream_read_file(self._locations, path) as gzip_stream:
|
||||||
|
with GzipFile(fileobj=gzip_stream) as unzipped:
|
||||||
|
unzipped_buffer = StringIO(unzipped.read())
|
||||||
|
return send_file(unzipped_buffer, mimetype=JSON_MIMETYPE)
|
||||||
|
except IOError:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
class LogArchive(object):
|
||||||
|
def __init__(self, app=None, distributed_storage=None):
|
||||||
|
self.app = app
|
||||||
|
if app is not None:
|
||||||
|
self.state = self.init_app(app, distributed_storage)
|
||||||
|
else:
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def init_app(self, app, distributed_storage):
|
||||||
|
location = app.config.get('LOG_ARCHIVE_LOCATION')
|
||||||
|
path = app.config.get('LOG_ARCHIVE_PATH', None)
|
||||||
|
|
||||||
|
handler_name = 'logarchive_handlers'
|
||||||
|
|
||||||
|
log_archive = DelegateUserfiles(app, distributed_storage, location, path, handler_name)
|
||||||
|
|
||||||
|
app.add_url_rule('/logarchive/<file_id>',
|
||||||
|
view_func=LogArchiveHandlers.as_view(handler_name,
|
||||||
|
distributed_storage=distributed_storage,
|
||||||
|
location=location,
|
||||||
|
files=log_archive))
|
||||||
|
|
||||||
|
# register extension with app
|
||||||
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
app.extensions['log_archive'] = log_archive
|
||||||
|
return log_archive
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.state, name, None)
|
|
@ -3,6 +3,8 @@ import stripe
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
|
|
||||||
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
PLANS = [
|
PLANS = [
|
||||||
# Deprecated Plans
|
# Deprecated Plans
|
||||||
{
|
{
|
||||||
|
@ -118,20 +120,6 @@ def get_plan(plan_id):
|
||||||
return None
|
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 FakeStripe(object):
|
||||||
class Customer(AttrDict):
|
class Customer(AttrDict):
|
||||||
FAKE_PLAN = AttrDict({
|
FAKE_PLAN = AttrDict({
|
||||||
|
|
|
@ -2,6 +2,11 @@ import redis
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from util.dynamic import import_class
|
from util.dynamic import import_class
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
ONE_DAY = timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
class BuildStatusRetrievalError(Exception):
|
class BuildStatusRetrievalError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -11,8 +16,8 @@ class RedisBuildLogs(object):
|
||||||
COMMAND = 'command'
|
COMMAND = 'command'
|
||||||
PHASE = 'phase'
|
PHASE = 'phase'
|
||||||
|
|
||||||
def __init__(self, redis_host):
|
def __init__(self, redis_config):
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _logs_key(build_id):
|
def _logs_key(build_id):
|
||||||
|
@ -25,7 +30,7 @@ class RedisBuildLogs(object):
|
||||||
"""
|
"""
|
||||||
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj))
|
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj))
|
||||||
|
|
||||||
def append_log_message(self, build_id, log_message, log_type=None):
|
def append_log_message(self, build_id, log_message, log_type=None, log_data=None):
|
||||||
"""
|
"""
|
||||||
Wraps the message in an envelope and push it to the end of the log entry
|
Wraps the message in an envelope and push it to the end of the log entry
|
||||||
list and returns the index at which it was inserted.
|
list and returns the index at which it was inserted.
|
||||||
|
@ -37,6 +42,9 @@ class RedisBuildLogs(object):
|
||||||
if log_type:
|
if log_type:
|
||||||
log_obj['type'] = log_type
|
log_obj['type'] = log_type
|
||||||
|
|
||||||
|
if log_data:
|
||||||
|
log_obj['data'] = log_data
|
||||||
|
|
||||||
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
|
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
|
||||||
|
|
||||||
def get_log_entries(self, build_id, start_index):
|
def get_log_entries(self, build_id, start_index):
|
||||||
|
@ -51,6 +59,13 @@ class RedisBuildLogs(object):
|
||||||
except redis.ConnectionError:
|
except redis.ConnectionError:
|
||||||
raise BuildStatusRetrievalError('Cannot retrieve build logs')
|
raise BuildStatusRetrievalError('Cannot retrieve build logs')
|
||||||
|
|
||||||
|
def expire_log_entries(self, build_id):
|
||||||
|
"""
|
||||||
|
Sets the log entry to expire in 1 day.
|
||||||
|
"""
|
||||||
|
self._redis.expire(self._logs_key(build_id), ONE_DAY)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _status_key(build_id):
|
def _status_key(build_id):
|
||||||
return 'builds/%s/status' % build_id
|
return 'builds/%s/status' % build_id
|
||||||
|
@ -89,7 +104,13 @@ class BuildLogs(object):
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
buildlogs_hostname = app.config.get('BUILDLOGS_REDIS_HOSTNAME')
|
buildlogs_config = app.config.get('BUILDLOGS_REDIS')
|
||||||
|
if not buildlogs_config:
|
||||||
|
# This is the old key name.
|
||||||
|
buildlogs_config = {
|
||||||
|
'host': app.config.get('BUILDLOGS_REDIS_HOSTNAME')
|
||||||
|
}
|
||||||
|
|
||||||
buildlogs_options = app.config.get('BUILDLOGS_OPTIONS', [])
|
buildlogs_options = app.config.get('BUILDLOGS_OPTIONS', [])
|
||||||
buildlogs_import = app.config.get('BUILDLOGS_MODULE_AND_CLASS', None)
|
buildlogs_import = app.config.get('BUILDLOGS_MODULE_AND_CLASS', None)
|
||||||
|
|
||||||
|
@ -98,7 +119,7 @@ class BuildLogs(object):
|
||||||
else:
|
else:
|
||||||
klass = import_class(buildlogs_import[0], buildlogs_import[1])
|
klass = import_class(buildlogs_import[0], buildlogs_import[1])
|
||||||
|
|
||||||
buildlogs = klass(buildlogs_hostname, *buildlogs_options)
|
buildlogs = klass(buildlogs_config, *buildlogs_options)
|
||||||
|
|
||||||
# register extension with app
|
# register extension with app
|
||||||
app.extensions = getattr(app, 'extensions', {})
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
@ -106,4 +127,4 @@ class BuildLogs(object):
|
||||||
return buildlogs
|
return buildlogs
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self.state, name, None)
|
return getattr(self.state, name, None)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from peewee import *
|
||||||
from data.read_slave import ReadSlaveModel
|
from data.read_slave import ReadSlaveModel
|
||||||
from sqlalchemy.engine.url import make_url
|
from sqlalchemy.engine.url import make_url
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
from util.names import urn_generator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -17,10 +17,28 @@ SCHEME_DRIVERS = {
|
||||||
'mysql': MySQLDatabase,
|
'mysql': MySQLDatabase,
|
||||||
'mysql+pymysql': MySQLDatabase,
|
'mysql+pymysql': MySQLDatabase,
|
||||||
'sqlite': SqliteDatabase,
|
'sqlite': SqliteDatabase,
|
||||||
|
'postgresql': PostgresqlDatabase,
|
||||||
|
'postgresql+psycopg2': PostgresqlDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHEME_RANDOM_FUNCTION = {
|
||||||
|
'mysql': fn.Rand,
|
||||||
|
'mysql+pymysql': fn.Rand,
|
||||||
|
'sqlite': fn.Random,
|
||||||
|
'postgresql': fn.Random,
|
||||||
|
'postgresql+psycopg2': fn.Random,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallableProxy(Proxy):
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.obj is None:
|
||||||
|
raise AttributeError('Cannot use uninitialized Proxy.')
|
||||||
|
return self.obj(*args, **kwargs)
|
||||||
|
|
||||||
db = Proxy()
|
db = Proxy()
|
||||||
read_slave = Proxy()
|
read_slave = Proxy()
|
||||||
|
db_random_func = CallableProxy()
|
||||||
|
|
||||||
|
|
||||||
def _db_from_url(url, db_kwargs):
|
def _db_from_url(url, db_kwargs):
|
||||||
parsed_url = make_url(url)
|
parsed_url = make_url(url)
|
||||||
|
@ -32,15 +50,19 @@ def _db_from_url(url, db_kwargs):
|
||||||
if parsed_url.username:
|
if parsed_url.username:
|
||||||
db_kwargs['user'] = parsed_url.username
|
db_kwargs['user'] = parsed_url.username
|
||||||
if parsed_url.password:
|
if parsed_url.password:
|
||||||
db_kwargs['passwd'] = parsed_url.password
|
db_kwargs['password'] = parsed_url.password
|
||||||
|
|
||||||
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def configure(config_object):
|
def configure(config_object):
|
||||||
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
|
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
|
||||||
write_db_uri = config_object['DB_URI']
|
write_db_uri = config_object['DB_URI']
|
||||||
db.initialize(_db_from_url(write_db_uri, db_kwargs))
|
db.initialize(_db_from_url(write_db_uri, db_kwargs))
|
||||||
|
|
||||||
|
parsed_write_uri = make_url(write_db_uri)
|
||||||
|
db_random_func.initialize(SCHEME_RANDOM_FUNCTION[parsed_write_uri.drivername])
|
||||||
|
|
||||||
read_slave_uri = config_object.get('DB_READ_SLAVE_URI', None)
|
read_slave_uri = config_object.get('DB_READ_SLAVE_URI', None)
|
||||||
if read_slave_uri is not None:
|
if read_slave_uri is not None:
|
||||||
read_slave.initialize(_db_from_url(read_slave_uri, db_kwargs))
|
read_slave.initialize(_db_from_url(read_slave_uri, db_kwargs))
|
||||||
|
@ -74,6 +96,8 @@ class User(BaseModel):
|
||||||
organization = BooleanField(default=False, index=True)
|
organization = BooleanField(default=False, index=True)
|
||||||
robot = BooleanField(default=False, index=True)
|
robot = BooleanField(default=False, index=True)
|
||||||
invoice_email = BooleanField(default=False)
|
invoice_email = BooleanField(default=False)
|
||||||
|
invalid_login_attempts = IntegerField(default=0)
|
||||||
|
last_invalid_login = DateTimeField(default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
class TeamRole(BaseModel):
|
class TeamRole(BaseModel):
|
||||||
|
@ -108,6 +132,15 @@ class TeamMember(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberInvite(BaseModel):
|
||||||
|
# Note: Either user OR email will be filled in, but not both.
|
||||||
|
user = ForeignKeyField(User, index=True, null=True)
|
||||||
|
email = CharField(null=True)
|
||||||
|
team = ForeignKeyField(Team, index=True)
|
||||||
|
inviter = ForeignKeyField(User, related_name='inviter')
|
||||||
|
invite_token = CharField(default=urn_generator(['teaminvite']))
|
||||||
|
|
||||||
|
|
||||||
class LoginService(BaseModel):
|
class LoginService(BaseModel):
|
||||||
name = CharField(unique=True, index=True)
|
name = CharField(unique=True, index=True)
|
||||||
|
|
||||||
|
@ -116,6 +149,7 @@ class FederatedLogin(BaseModel):
|
||||||
user = ForeignKeyField(User, index=True)
|
user = ForeignKeyField(User, index=True)
|
||||||
service = ForeignKeyField(LoginService, index=True)
|
service = ForeignKeyField(LoginService, index=True)
|
||||||
service_ident = CharField()
|
service_ident = CharField()
|
||||||
|
metadata_json = TextField(default='{}')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
@ -134,7 +168,7 @@ class Visibility(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Repository(BaseModel):
|
class Repository(BaseModel):
|
||||||
namespace = CharField()
|
namespace_user = ForeignKeyField(User)
|
||||||
name = CharField()
|
name = CharField()
|
||||||
visibility = ForeignKeyField(Visibility)
|
visibility = ForeignKeyField(Visibility)
|
||||||
description = TextField(null=True)
|
description = TextField(null=True)
|
||||||
|
@ -145,7 +179,7 @@ class Repository(BaseModel):
|
||||||
read_slaves = (read_slave,)
|
read_slaves = (read_slave,)
|
||||||
indexes = (
|
indexes = (
|
||||||
# create a unique index on namespace and name
|
# create a unique index on namespace and name
|
||||||
(('namespace', 'name'), True),
|
(('namespace_user', 'name'), True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -229,6 +263,7 @@ class ImageStorage(BaseModel):
|
||||||
comment = TextField(null=True)
|
comment = TextField(null=True)
|
||||||
command = TextField(null=True)
|
command = TextField(null=True)
|
||||||
image_size = BigIntegerField(null=True)
|
image_size = BigIntegerField(null=True)
|
||||||
|
uncompressed_size = BigIntegerField(null=True)
|
||||||
uploading = BooleanField(default=True, null=True)
|
uploading = BooleanField(default=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,6 +319,16 @@ class RepositoryTag(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BUILD_PHASE(object):
|
||||||
|
""" Build phases enum """
|
||||||
|
ERROR = 'error'
|
||||||
|
UNPACKING = 'unpacking'
|
||||||
|
PULLING = 'pulling'
|
||||||
|
BUILDING = 'building'
|
||||||
|
PUSHING = 'pushing'
|
||||||
|
COMPLETE = 'complete'
|
||||||
|
|
||||||
|
|
||||||
class RepositoryBuild(BaseModel):
|
class RepositoryBuild(BaseModel):
|
||||||
uuid = CharField(default=uuid_generator, index=True)
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
repository = ForeignKeyField(Repository, index=True)
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
|
@ -295,12 +340,13 @@ class RepositoryBuild(BaseModel):
|
||||||
display_name = CharField()
|
display_name = CharField()
|
||||||
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
|
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
|
||||||
pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot')
|
pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot')
|
||||||
|
logs_archived = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class QueueItem(BaseModel):
|
class QueueItem(BaseModel):
|
||||||
queue_name = CharField(index=True, max_length=1024)
|
queue_name = CharField(index=True, max_length=1024)
|
||||||
body = TextField()
|
body = TextField()
|
||||||
available_after = DateTimeField(default=datetime.now, index=True)
|
available_after = DateTimeField(default=datetime.utcnow, index=True)
|
||||||
available = BooleanField(default=True, index=True)
|
available = BooleanField(default=True, index=True)
|
||||||
processing_expires = DateTimeField(null=True, index=True)
|
processing_expires = DateTimeField(null=True, index=True)
|
||||||
retries_remaining = IntegerField(default=5)
|
retries_remaining = IntegerField(default=5)
|
||||||
|
@ -405,4 +451,4 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission,
|
||||||
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
||||||
Notification, ImageStorageLocation, ImageStoragePlacement,
|
Notification, ImageStorageLocation, ImageStoragePlacement,
|
||||||
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
||||||
RepositoryAuthorizedEmail]
|
RepositoryAuthorizedEmail, TeamMemberInvite]
|
||||||
|
|
|
@ -8,6 +8,7 @@ from peewee import SqliteDatabase
|
||||||
from data.database import all_models, db
|
from data.database import all_models, db
|
||||||
from app import app
|
from app import app
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
||||||
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
|
@ -23,6 +24,7 @@ fileConfig(config.config_file_name)
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
# target_metadata = mymodel.Base.metadata
|
# target_metadata = mymodel.Base.metadata
|
||||||
target_metadata = gen_sqlalchemy_metadata(all_models)
|
target_metadata = gen_sqlalchemy_metadata(all_models)
|
||||||
|
tables = AttrDict(target_metadata.tables)
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
|
@ -45,7 +47,7 @@ def run_migrations_offline():
|
||||||
context.configure(url=url, target_metadata=target_metadata, transactional_ddl=True)
|
context.configure(url=url, target_metadata=target_metadata, transactional_ddl=True)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations(tables=tables)
|
||||||
|
|
||||||
def run_migrations_online():
|
def run_migrations_online():
|
||||||
"""Run migrations in 'online' mode.
|
"""Run migrations in 'online' mode.
|
||||||
|
@ -72,7 +74,7 @@ def run_migrations_online():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations(tables=tables)
|
||||||
finally:
|
finally:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
def upgrade():
|
def upgrade(tables):
|
||||||
${upgrades if upgrades else "pass"}
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
${downgrades if downgrades else "pass"}
|
${downgrades if downgrades else "pass"}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Migrate registry namespaces to reference a user.
|
||||||
|
|
||||||
|
Revision ID: 13da56878560
|
||||||
|
Revises: 51d04d0e7e6f
|
||||||
|
Create Date: 2014-09-18 13:56:45.130455
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '13da56878560'
|
||||||
|
down_revision = '51d04d0e7e6f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from data.database import Repository, User
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
# Add the namespace_user column, allowing it to be nullable
|
||||||
|
op.add_column('repository', sa.Column('namespace_user_id', sa.Integer(), sa.ForeignKey('user.id')))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.drop_column('repository', 'namespace_user_id')
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""add metadata field to external logins
|
||||||
|
|
||||||
|
Revision ID: 1594a74a74ca
|
||||||
|
Revises: f42b0ea7a4d
|
||||||
|
Create Date: 2014-09-04 18:17:35.205698
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1594a74a74ca'
|
||||||
|
down_revision = 'f42b0ea7a4d'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('federatedlogin', sa.Column('metadata_json', sa.Text(), nullable=False))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
op.bulk_insert(tables.loginservice,
|
||||||
|
[
|
||||||
|
{'id':4, 'name':'google'},
|
||||||
|
])
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('federatedlogin', 'metadata_json')
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.loginservice.delete()
|
||||||
|
.where(tables.loginservice.c.name == op.inline_literal('google')))
|
||||||
|
)
|
|
@ -14,7 +14,7 @@ from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
def upgrade():
|
def upgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice')
|
op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice')
|
||||||
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=True)
|
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=True)
|
||||||
|
@ -34,7 +34,7 @@ def upgrade():
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index('visibility_name', table_name='visibility')
|
op.drop_index('visibility_name', table_name='visibility')
|
||||||
op.create_index('visibility_name', 'visibility', ['name'], unique=False)
|
op.create_index('visibility_name', 'visibility', ['name'], unique=False)
|
||||||
|
|
|
@ -13,12 +13,8 @@ down_revision = '4b7ef0c7bdb2'
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
|
||||||
from data.database import all_models
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('externalnotificationmethod',
|
op.create_table('externalnotificationmethod',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
@ -26,7 +22,7 @@ def upgrade():
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
op.create_index('externalnotificationmethod_name', 'externalnotificationmethod', ['name'], unique=True)
|
op.create_index('externalnotificationmethod_name', 'externalnotificationmethod', ['name'], unique=True)
|
||||||
op.bulk_insert(schema.tables['externalnotificationmethod'],
|
op.bulk_insert(tables.externalnotificationmethod,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'quay_notification'},
|
{'id':1, 'name':'quay_notification'},
|
||||||
{'id':2, 'name':'email'},
|
{'id':2, 'name':'email'},
|
||||||
|
@ -38,7 +34,7 @@ def upgrade():
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
op.create_index('externalnotificationevent_name', 'externalnotificationevent', ['name'], unique=True)
|
op.create_index('externalnotificationevent_name', 'externalnotificationevent', ['name'], unique=True)
|
||||||
op.bulk_insert(schema.tables['externalnotificationevent'],
|
op.bulk_insert(tables.externalnotificationevent,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'repo_push'},
|
{'id':1, 'name':'repo_push'},
|
||||||
{'id':2, 'name':'build_queued'},
|
{'id':2, 'name':'build_queued'},
|
||||||
|
@ -77,7 +73,7 @@ def upgrade():
|
||||||
op.add_column(u'notification', sa.Column('dismissed', sa.Boolean(), nullable=False))
|
op.add_column(u'notification', sa.Column('dismissed', sa.Boolean(), nullable=False))
|
||||||
|
|
||||||
# Manually add the new notificationkind types
|
# Manually add the new notificationkind types
|
||||||
op.bulk_insert(schema.tables['notificationkind'],
|
op.bulk_insert(tables.notificationkind,
|
||||||
[
|
[
|
||||||
{'id':5, 'name':'repo_push'},
|
{'id':5, 'name':'repo_push'},
|
||||||
{'id':6, 'name':'build_queued'},
|
{'id':6, 'name':'build_queued'},
|
||||||
|
@ -87,7 +83,7 @@ def upgrade():
|
||||||
])
|
])
|
||||||
|
|
||||||
# Manually add the new logentrykind types
|
# Manually add the new logentrykind types
|
||||||
op.bulk_insert(schema.tables['logentrykind'],
|
op.bulk_insert(tables.logentrykind,
|
||||||
[
|
[
|
||||||
{'id':39, 'name':'add_repo_notification'},
|
{'id':39, 'name':'add_repo_notification'},
|
||||||
{'id':40, 'name':'delete_repo_notification'},
|
{'id':40, 'name':'delete_repo_notification'},
|
||||||
|
@ -97,61 +93,49 @@ def upgrade():
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_column(u'notification', 'dismissed')
|
op.drop_column(u'notification', 'dismissed')
|
||||||
op.drop_index('repositorynotification_uuid', table_name='repositorynotification')
|
|
||||||
op.drop_index('repositorynotification_repository_id', table_name='repositorynotification')
|
|
||||||
op.drop_index('repositorynotification_method_id', table_name='repositorynotification')
|
|
||||||
op.drop_index('repositorynotification_event_id', table_name='repositorynotification')
|
|
||||||
op.drop_table('repositorynotification')
|
op.drop_table('repositorynotification')
|
||||||
op.drop_index('repositoryauthorizedemail_repository_id', table_name='repositoryauthorizedemail')
|
|
||||||
op.drop_index('repositoryauthorizedemail_email_repository_id', table_name='repositoryauthorizedemail')
|
|
||||||
op.drop_index('repositoryauthorizedemail_code', table_name='repositoryauthorizedemail')
|
|
||||||
op.drop_table('repositoryauthorizedemail')
|
op.drop_table('repositoryauthorizedemail')
|
||||||
op.drop_index('externalnotificationevent_name', table_name='externalnotificationevent')
|
|
||||||
op.drop_table('externalnotificationevent')
|
op.drop_table('externalnotificationevent')
|
||||||
op.drop_index('externalnotificationmethod_name', table_name='externalnotificationmethod')
|
|
||||||
op.drop_table('externalnotificationmethod')
|
op.drop_table('externalnotificationmethod')
|
||||||
|
|
||||||
# Manually remove the notificationkind and logentrykind types
|
# Manually remove the notificationkind and logentrykind types
|
||||||
notificationkind = schema.tables['notificationkind']
|
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('repo_push')))
|
.where(tables.notificationkind.c.name == op.inline_literal('repo_push')))
|
||||||
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('build_queued')))
|
.where(tables.notificationkind.c.name == op.inline_literal('build_queued')))
|
||||||
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('build_start')))
|
.where(tables.notificationkind.c.name == op.inline_literal('build_start')))
|
||||||
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('build_success')))
|
.where(tables.notificationkind.c.name == op.inline_literal('build_success')))
|
||||||
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('build_failure')))
|
.where(tables.notificationkind.c.name == op.inline_literal('build_failure')))
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
op.execute(
|
op.execute(
|
||||||
(logentrykind.delete()
|
(tables.logentrykind.delete()
|
||||||
.where(logentrykind.c.name == op.inline_literal('add_repo_notification')))
|
.where(tables.logentrykind.c.name == op.inline_literal('add_repo_notification')))
|
||||||
|
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute(
|
||||||
(logentrykind.delete()
|
(tables.logentrykind.delete()
|
||||||
.where(logentrykind.c.name == op.inline_literal('delete_repo_notification')))
|
.where(tables.logentrykind.c.name == op.inline_literal('delete_repo_notification')))
|
||||||
|
|
||||||
)
|
)
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Add support for build log migration.
|
||||||
|
|
||||||
|
Revision ID: 34fd69f63809
|
||||||
|
Revises: 4a0c94399f38
|
||||||
|
Create Date: 2014-09-12 11:50:09.217777
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '34fd69f63809'
|
||||||
|
down_revision = '4a0c94399f38'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('repositorybuild', sa.Column('logs_archived', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('repositorybuild', 'logs_archived')
|
||||||
|
### end Alembic commands ###
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Backfill the namespace_user fields.
|
||||||
|
|
||||||
|
Revision ID: 3f4fe1194671
|
||||||
|
Revises: 6f2ecf5afcf
|
||||||
|
Create Date: 2014-09-24 14:29:45.192179
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3f4fe1194671'
|
||||||
|
down_revision = '6f2ecf5afcf'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
conn = op.get_bind()
|
||||||
|
conn.execute('update repository set namespace_user_id = (select id from user where user.username = repository.namespace) where namespace_user_id is NULL')
|
||||||
|
|
||||||
|
op.create_index('repository_namespace_user_id_name', 'repository', ['namespace_user_id', 'name'], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.drop_constraint('fk_repository_namespace_user_id_user', table_name='repository', type_='foreignkey')
|
||||||
|
op.drop_index('repository_namespace_user_id_name', table_name='repository')
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""add log kind for regenerating robot tokens
|
||||||
|
|
||||||
|
Revision ID: 43e943c0639f
|
||||||
|
Revises: 82297d834ad
|
||||||
|
Create Date: 2014-08-25 17:14:42.784518
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '43e943c0639f'
|
||||||
|
down_revision = '82297d834ad'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.bulk_insert(tables.logentrykind,
|
||||||
|
[
|
||||||
|
{'id': 41, 'name':'regenerate_robot_token'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('regenerate_robot_token')))
|
||||||
|
|
||||||
|
)
|
|
@ -18,14 +18,14 @@ def get_id(query):
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
return list(conn.execute(query, ()).fetchall())[0][0]
|
return list(conn.execute(query, ()).fetchall())[0][0]
|
||||||
|
|
||||||
def upgrade():
|
def upgrade(tables):
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
event_id = get_id('Select id From externalnotificationevent Where name="repo_push" Limit 1')
|
event_id = get_id('Select id From externalnotificationevent Where name=\'repo_push\' Limit 1')
|
||||||
method_id = get_id('Select id From externalnotificationmethod Where name="webhook" Limit 1')
|
method_id = get_id('Select id From externalnotificationmethod Where name=\'webhook\' Limit 1')
|
||||||
conn.execute('Insert Into repositorynotification (uuid, repository_id, event_id, method_id, config_json) Select public_id, repository_id, %s, %s, parameters FROM webhook' % (event_id, method_id))
|
conn.execute('Insert Into repositorynotification (uuid, repository_id, event_id, method_id, config_json) Select public_id, repository_id, %s, %s, parameters FROM webhook' % (event_id, method_id))
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
event_id = get_id('Select id From externalnotificationevent Where name="repo_push" Limit 1')
|
event_id = get_id('Select id From externalnotificationevent Where name=\'repo_push\' Limit 1')
|
||||||
method_id = get_id('Select id From externalnotificationmethod Where name="webhook" Limit 1')
|
method_id = get_id('Select id From externalnotificationmethod Where name=\'webhook\' Limit 1')
|
||||||
conn.execute('Insert Into webhook (public_id, repository_id, parameters) Select uuid, repository_id, config_json FROM repositorynotification Where event_id=%s And method_id=%s' % (event_id, method_id))
|
conn.execute('Insert Into webhook (public_id, repository_id, parameters) Select uuid, repository_id, config_json FROM repositorynotification Where event_id=%s And method_id=%s' % (event_id, method_id))
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""add new notification kinds
|
||||||
|
|
||||||
|
Revision ID: 4a0c94399f38
|
||||||
|
Revises: 1594a74a74ca
|
||||||
|
Create Date: 2014-08-28 16:17:01.898269
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4a0c94399f38'
|
||||||
|
down_revision = '1594a74a74ca'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.bulk_insert(tables.externalnotificationmethod,
|
||||||
|
[
|
||||||
|
{'id':4, 'name':'flowdock'},
|
||||||
|
{'id':5, 'name':'hipchat'},
|
||||||
|
{'id':6, 'name':'slack'},
|
||||||
|
])
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.execute(
|
||||||
|
(tables.externalnotificationmethod.delete()
|
||||||
|
.where(tables.externalnotificationmethod.c.name == op.inline_literal('flowdock')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.externalnotificationmethod.delete()
|
||||||
|
.where(tables.externalnotificationmethod.c.name == op.inline_literal('hipchat')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.externalnotificationmethod.delete()
|
||||||
|
.where(tables.externalnotificationmethod.c.name == op.inline_literal('slack')))
|
||||||
|
)
|
|
@ -11,23 +11,18 @@ revision = '4b7ef0c7bdb2'
|
||||||
down_revision = 'bcdde200a1b'
|
down_revision = 'bcdde200a1b'
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
|
||||||
from data.database import all_models
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
def upgrade():
|
op.bulk_insert(tables.notificationkind,
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
op.bulk_insert(schema.tables['notificationkind'],
|
|
||||||
[
|
[
|
||||||
{'id':4, 'name':'maintenance'},
|
{'id':4, 'name':'maintenance'},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
notificationkind = schema.tables['notificationkind']
|
|
||||||
op.execute(
|
op.execute(
|
||||||
(notificationkind.delete()
|
(tables.notificationkind.delete()
|
||||||
.where(notificationkind.c.name == op.inline_literal('maintenance')))
|
.where(tables.notificationkind.c.name == op.inline_literal('maintenance')))
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Add brute force prevention metadata to the user table.
|
||||||
|
|
||||||
|
Revision ID: 4fdb65816b8d
|
||||||
|
Revises: 43e943c0639f
|
||||||
|
Create Date: 2014-09-03 12:35:33.722435
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4fdb65816b8d'
|
||||||
|
down_revision = '43e943c0639f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('invalid_login_attempts', sa.Integer(), nullable=False, server_default="0"))
|
||||||
|
op.add_column('user', sa.Column('last_invalid_login', sa.DateTime(), nullable=False, server_default=sa.func.now()))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'last_invalid_login')
|
||||||
|
op.drop_column('user', 'invalid_login_attempts')
|
||||||
|
### end Alembic commands ###
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""Email invites for joining a team.
|
||||||
|
|
||||||
|
Revision ID: 51d04d0e7e6f
|
||||||
|
Revises: 34fd69f63809
|
||||||
|
Create Date: 2014-09-15 23:51:35.478232
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '51d04d0e7e6f'
|
||||||
|
down_revision = '34fd69f63809'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('teammemberinvite',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('team_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('inviter_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('invite_token', sa.String(length=255), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['inviter_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['team_id'], ['team.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('teammemberinvite_inviter_id', 'teammemberinvite', ['inviter_id'], unique=False)
|
||||||
|
op.create_index('teammemberinvite_team_id', 'teammemberinvite', ['team_id'], unique=False)
|
||||||
|
op.create_index('teammemberinvite_user_id', 'teammemberinvite', ['user_id'], unique=False)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
# Manually add the new logentrykind types
|
||||||
|
op.bulk_insert(tables.logentrykind,
|
||||||
|
[
|
||||||
|
{'id':42, 'name':'org_invite_team_member'},
|
||||||
|
{'id':43, 'name':'org_team_member_invite_accepted'},
|
||||||
|
{'id':44, 'name':'org_team_member_invite_declined'},
|
||||||
|
{'id':45, 'name':'org_delete_team_member_invite'},
|
||||||
|
])
|
||||||
|
|
||||||
|
op.bulk_insert(tables.notificationkind,
|
||||||
|
[
|
||||||
|
{'id':10, 'name':'org_team_invite'},
|
||||||
|
])
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_invite_team_member')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_team_member_invite_accepted')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_team_member_invite_declined')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_delete_team_member_invite')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.notificationkind.delete()
|
||||||
|
.where(tables.notificationkind.c.name == op.inline_literal('org_team_invite')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_table('teammemberinvite')
|
||||||
|
### end Alembic commands ###
|
|
@ -11,14 +11,9 @@ revision = '5a07499ce53f'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
|
||||||
from data.database import all_models
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
def upgrade():
|
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('loginservice',
|
op.create_table('loginservice',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
@ -27,7 +22,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('loginservice_name', 'loginservice', ['name'], unique=True)
|
op.create_index('loginservice_name', 'loginservice', ['name'], unique=True)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['loginservice'],
|
op.bulk_insert(tables.loginservice,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'github'},
|
{'id':1, 'name':'github'},
|
||||||
{'id':2, 'name':'quayrobot'},
|
{'id':2, 'name':'quayrobot'},
|
||||||
|
@ -66,7 +61,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('role_name', 'role', ['name'], unique=False)
|
op.create_index('role_name', 'role', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['role'],
|
op.bulk_insert(tables.role,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'admin'},
|
{'id':1, 'name':'admin'},
|
||||||
{'id':2, 'name':'write'},
|
{'id':2, 'name':'write'},
|
||||||
|
@ -80,7 +75,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('logentrykind_name', 'logentrykind', ['name'], unique=False)
|
op.create_index('logentrykind_name', 'logentrykind', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['logentrykind'],
|
op.bulk_insert(tables.logentrykind,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'account_change_plan'},
|
{'id':1, 'name':'account_change_plan'},
|
||||||
{'id':2, 'name':'account_change_cc'},
|
{'id':2, 'name':'account_change_cc'},
|
||||||
|
@ -136,7 +131,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('notificationkind_name', 'notificationkind', ['name'], unique=False)
|
op.create_index('notificationkind_name', 'notificationkind', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['notificationkind'],
|
op.bulk_insert(tables.notificationkind,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'password_required'},
|
{'id':1, 'name':'password_required'},
|
||||||
{'id':2, 'name':'over_private_usage'},
|
{'id':2, 'name':'over_private_usage'},
|
||||||
|
@ -150,7 +145,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('teamrole_name', 'teamrole', ['name'], unique=False)
|
op.create_index('teamrole_name', 'teamrole', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['teamrole'],
|
op.bulk_insert(tables.teamrole,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'admin'},
|
{'id':1, 'name':'admin'},
|
||||||
{'id':2, 'name':'creator'},
|
{'id':2, 'name':'creator'},
|
||||||
|
@ -164,7 +159,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('visibility_name', 'visibility', ['name'], unique=False)
|
op.create_index('visibility_name', 'visibility', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['visibility'],
|
op.bulk_insert(tables.visibility,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'public'},
|
{'id':1, 'name':'public'},
|
||||||
{'id':2, 'name':'private'},
|
{'id':2, 'name':'private'},
|
||||||
|
@ -194,7 +189,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=False)
|
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=False)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['buildtriggerservice'],
|
op.bulk_insert(tables.buildtriggerservice,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'github'},
|
{'id':1, 'name':'github'},
|
||||||
])
|
])
|
||||||
|
@ -203,7 +198,7 @@ def upgrade():
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('service_id', sa.Integer(), nullable=False),
|
sa.Column('service_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('service_ident', sa.String(length=255, collation='utf8_general_ci'), nullable=False),
|
sa.Column('service_ident', sa.String(length=255), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['service_id'], ['loginservice.id'], ),
|
sa.ForeignKeyConstraint(['service_id'], ['loginservice.id'], ),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
@ -375,7 +370,7 @@ def upgrade():
|
||||||
sa.Column('command', sa.Text(), nullable=True),
|
sa.Column('command', sa.Text(), nullable=True),
|
||||||
sa.Column('repository_id', sa.Integer(), nullable=False),
|
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('image_size', sa.BigInteger(), nullable=True),
|
sa.Column('image_size', sa.BigInteger(), nullable=True),
|
||||||
sa.Column('ancestors', sa.String(length=60535, collation='latin1_swedish_ci'), nullable=True),
|
sa.Column('ancestors', sa.String(length=60535), nullable=True),
|
||||||
sa.Column('storage_id', sa.Integer(), nullable=True),
|
sa.Column('storage_id', sa.Integer(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||||
sa.ForeignKeyConstraint(['storage_id'], ['imagestorage.id'], ),
|
sa.ForeignKeyConstraint(['storage_id'], ['imagestorage.id'], ),
|
||||||
|
@ -490,119 +485,34 @@ def upgrade():
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index('repositorybuild_uuid', table_name='repositorybuild')
|
|
||||||
op.drop_index('repositorybuild_trigger_id', table_name='repositorybuild')
|
|
||||||
op.drop_index('repositorybuild_resource_key', table_name='repositorybuild')
|
|
||||||
op.drop_index('repositorybuild_repository_id', table_name='repositorybuild')
|
|
||||||
op.drop_index('repositorybuild_pull_robot_id', table_name='repositorybuild')
|
|
||||||
op.drop_index('repositorybuild_access_token_id', table_name='repositorybuild')
|
|
||||||
op.drop_table('repositorybuild')
|
op.drop_table('repositorybuild')
|
||||||
op.drop_index('repositorybuildtrigger_write_token_id', table_name='repositorybuildtrigger')
|
|
||||||
op.drop_index('repositorybuildtrigger_service_id', table_name='repositorybuildtrigger')
|
|
||||||
op.drop_index('repositorybuildtrigger_repository_id', table_name='repositorybuildtrigger')
|
|
||||||
op.drop_index('repositorybuildtrigger_pull_robot_id', table_name='repositorybuildtrigger')
|
|
||||||
op.drop_index('repositorybuildtrigger_connected_user_id', table_name='repositorybuildtrigger')
|
|
||||||
op.drop_table('repositorybuildtrigger')
|
op.drop_table('repositorybuildtrigger')
|
||||||
op.drop_index('logentry_repository_id', table_name='logentry')
|
|
||||||
op.drop_index('logentry_performer_id', table_name='logentry')
|
|
||||||
op.drop_index('logentry_kind_id', table_name='logentry')
|
|
||||||
op.drop_index('logentry_datetime', table_name='logentry')
|
|
||||||
op.drop_index('logentry_account_id', table_name='logentry')
|
|
||||||
op.drop_index('logentry_access_token_id', table_name='logentry')
|
|
||||||
op.drop_table('logentry')
|
op.drop_table('logentry')
|
||||||
op.drop_index('repositorytag_repository_id_name', table_name='repositorytag')
|
|
||||||
op.drop_index('repositorytag_repository_id', table_name='repositorytag')
|
|
||||||
op.drop_index('repositorytag_image_id', table_name='repositorytag')
|
|
||||||
op.drop_table('repositorytag')
|
op.drop_table('repositorytag')
|
||||||
op.drop_index('permissionprototype_role_id', table_name='permissionprototype')
|
|
||||||
op.drop_index('permissionprototype_org_id_activating_user_id', table_name='permissionprototype')
|
|
||||||
op.drop_index('permissionprototype_org_id', table_name='permissionprototype')
|
|
||||||
op.drop_index('permissionprototype_delegate_user_id', table_name='permissionprototype')
|
|
||||||
op.drop_index('permissionprototype_delegate_team_id', table_name='permissionprototype')
|
|
||||||
op.drop_index('permissionprototype_activating_user_id', table_name='permissionprototype')
|
|
||||||
op.drop_table('permissionprototype')
|
op.drop_table('permissionprototype')
|
||||||
op.drop_index('image_storage_id', table_name='image')
|
|
||||||
op.drop_index('image_repository_id_docker_image_id', table_name='image')
|
|
||||||
op.drop_index('image_repository_id', table_name='image')
|
|
||||||
op.drop_index('image_ancestors', table_name='image')
|
|
||||||
op.drop_table('image')
|
op.drop_table('image')
|
||||||
op.drop_index('oauthauthorizationcode_code', table_name='oauthauthorizationcode')
|
|
||||||
op.drop_index('oauthauthorizationcode_application_id', table_name='oauthauthorizationcode')
|
|
||||||
op.drop_table('oauthauthorizationcode')
|
op.drop_table('oauthauthorizationcode')
|
||||||
op.drop_index('webhook_repository_id', table_name='webhook')
|
|
||||||
op.drop_index('webhook_public_id', table_name='webhook')
|
|
||||||
op.drop_table('webhook')
|
op.drop_table('webhook')
|
||||||
op.drop_index('teammember_user_id_team_id', table_name='teammember')
|
|
||||||
op.drop_index('teammember_user_id', table_name='teammember')
|
|
||||||
op.drop_index('teammember_team_id', table_name='teammember')
|
|
||||||
op.drop_table('teammember')
|
op.drop_table('teammember')
|
||||||
op.drop_index('oauthaccesstoken_uuid', table_name='oauthaccesstoken')
|
|
||||||
op.drop_index('oauthaccesstoken_refresh_token', table_name='oauthaccesstoken')
|
|
||||||
op.drop_index('oauthaccesstoken_authorized_user_id', table_name='oauthaccesstoken')
|
|
||||||
op.drop_index('oauthaccesstoken_application_id', table_name='oauthaccesstoken')
|
|
||||||
op.drop_index('oauthaccesstoken_access_token', table_name='oauthaccesstoken')
|
|
||||||
op.drop_table('oauthaccesstoken')
|
op.drop_table('oauthaccesstoken')
|
||||||
op.drop_index('repositorypermission_user_id_repository_id', table_name='repositorypermission')
|
|
||||||
op.drop_index('repositorypermission_user_id', table_name='repositorypermission')
|
|
||||||
op.drop_index('repositorypermission_team_id_repository_id', table_name='repositorypermission')
|
|
||||||
op.drop_index('repositorypermission_team_id', table_name='repositorypermission')
|
|
||||||
op.drop_index('repositorypermission_role_id', table_name='repositorypermission')
|
|
||||||
op.drop_index('repositorypermission_repository_id', table_name='repositorypermission')
|
|
||||||
op.drop_table('repositorypermission')
|
op.drop_table('repositorypermission')
|
||||||
op.drop_index('accesstoken_role_id', table_name='accesstoken')
|
|
||||||
op.drop_index('accesstoken_repository_id', table_name='accesstoken')
|
|
||||||
op.drop_index('accesstoken_code', table_name='accesstoken')
|
|
||||||
op.drop_table('accesstoken')
|
op.drop_table('accesstoken')
|
||||||
op.drop_index('repository_visibility_id', table_name='repository')
|
|
||||||
op.drop_index('repository_namespace_name', table_name='repository')
|
|
||||||
op.drop_table('repository')
|
op.drop_table('repository')
|
||||||
op.drop_index('team_role_id', table_name='team')
|
|
||||||
op.drop_index('team_organization_id', table_name='team')
|
|
||||||
op.drop_index('team_name_organization_id', table_name='team')
|
|
||||||
op.drop_index('team_name', table_name='team')
|
|
||||||
op.drop_table('team')
|
op.drop_table('team')
|
||||||
op.drop_index('emailconfirmation_user_id', table_name='emailconfirmation')
|
|
||||||
op.drop_index('emailconfirmation_code', table_name='emailconfirmation')
|
|
||||||
op.drop_table('emailconfirmation')
|
op.drop_table('emailconfirmation')
|
||||||
op.drop_index('notification_uuid', table_name='notification')
|
|
||||||
op.drop_index('notification_target_id', table_name='notification')
|
|
||||||
op.drop_index('notification_kind_id', table_name='notification')
|
|
||||||
op.drop_index('notification_created', table_name='notification')
|
|
||||||
op.drop_table('notification')
|
op.drop_table('notification')
|
||||||
op.drop_index('oauthapplication_organization_id', table_name='oauthapplication')
|
|
||||||
op.drop_index('oauthapplication_client_id', table_name='oauthapplication')
|
|
||||||
op.drop_table('oauthapplication')
|
op.drop_table('oauthapplication')
|
||||||
op.drop_index('federatedlogin_user_id', table_name='federatedlogin')
|
|
||||||
op.drop_index('federatedlogin_service_id_user_id', table_name='federatedlogin')
|
|
||||||
op.drop_index('federatedlogin_service_id_service_ident', table_name='federatedlogin')
|
|
||||||
op.drop_index('federatedlogin_service_id', table_name='federatedlogin')
|
|
||||||
op.drop_table('federatedlogin')
|
op.drop_table('federatedlogin')
|
||||||
op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice')
|
|
||||||
op.drop_table('buildtriggerservice')
|
op.drop_table('buildtriggerservice')
|
||||||
op.drop_index('user_username', table_name='user')
|
|
||||||
op.drop_index('user_stripe_id', table_name='user')
|
|
||||||
op.drop_index('user_robot', table_name='user')
|
|
||||||
op.drop_index('user_organization', table_name='user')
|
|
||||||
op.drop_index('user_email', table_name='user')
|
|
||||||
op.drop_table('user')
|
op.drop_table('user')
|
||||||
op.drop_index('visibility_name', table_name='visibility')
|
|
||||||
op.drop_table('visibility')
|
op.drop_table('visibility')
|
||||||
op.drop_index('teamrole_name', table_name='teamrole')
|
|
||||||
op.drop_table('teamrole')
|
op.drop_table('teamrole')
|
||||||
op.drop_index('notificationkind_name', table_name='notificationkind')
|
|
||||||
op.drop_table('notificationkind')
|
op.drop_table('notificationkind')
|
||||||
op.drop_index('logentrykind_name', table_name='logentrykind')
|
|
||||||
op.drop_table('logentrykind')
|
op.drop_table('logentrykind')
|
||||||
op.drop_index('role_name', table_name='role')
|
|
||||||
op.drop_table('role')
|
op.drop_table('role')
|
||||||
op.drop_index('queueitem_queue_name', table_name='queueitem')
|
|
||||||
op.drop_index('queueitem_processing_expires', table_name='queueitem')
|
|
||||||
op.drop_index('queueitem_available_after', table_name='queueitem')
|
|
||||||
op.drop_index('queueitem_available', table_name='queueitem')
|
|
||||||
op.drop_table('queueitem')
|
op.drop_table('queueitem')
|
||||||
op.drop_table('imagestorage')
|
op.drop_table('imagestorage')
|
||||||
op.drop_index('loginservice_name', table_name='loginservice')
|
|
||||||
op.drop_table('loginservice')
|
op.drop_table('loginservice')
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""add the uncompressed size to image storage
|
||||||
|
|
||||||
|
Revision ID: 6f2ecf5afcf
|
||||||
|
Revises: 13da56878560
|
||||||
|
Create Date: 2014-09-22 14:39:13.470566
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6f2ecf5afcf'
|
||||||
|
down_revision = '13da56878560'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('imagestorage', sa.Column('uncompressed_size', sa.BigInteger(), nullable=True))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('imagestorage', 'uncompressed_size')
|
||||||
|
### end Alembic commands ###
|
|
@ -13,24 +13,17 @@ down_revision = '47670cbeced'
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
|
||||||
from data.database import all_models
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
def upgrade():
|
op.bulk_insert(tables.imagestoragelocation,
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['imagestoragelocation'],
|
|
||||||
[
|
[
|
||||||
{'id':8, 'name':'s3_us_west_1'},
|
{'id':8, 'name':'s3_us_west_1'},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
op.execute(
|
op.execute(
|
||||||
(imagestoragelocation.delete()
|
(tables.imagestoragelocation.delete()
|
||||||
.where(imagestoragelocation.c.name == op.inline_literal('s3_us_west_1')))
|
.where(tables.imagestoragelocation.c.name == op.inline_literal('s3_us_west_1')))
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Allow the namespace column to be nullable.
|
||||||
|
|
||||||
|
Revision ID: 9a1087b007d
|
||||||
|
Revises: 3f4fe1194671
|
||||||
|
Create Date: 2014-10-01 16:11:21.277226
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9a1087b007d'
|
||||||
|
down_revision = '3f4fe1194671'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.drop_index('repository_namespace_name', table_name='repository')
|
||||||
|
op.alter_column('repository', 'namespace', nullable=True, existing_type=sa.String(length=255),
|
||||||
|
server_default=sa.text('NULL'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
conn = op.get_bind()
|
||||||
|
conn.execute('update repository set namespace = (select username from user where user.id = repository.namespace_user_id) where namespace is NULL')
|
||||||
|
|
||||||
|
op.create_index('repository_namespace_name', 'repository', ['namespace', 'name'], unique=True)
|
||||||
|
op.alter_column('repository', 'namespace', nullable=False, existing_type=sa.String(length=255))
|
|
@ -11,14 +11,10 @@ revision = 'bcdde200a1b'
|
||||||
down_revision = '201d55b38649'
|
down_revision = '201d55b38649'
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
|
||||||
from data.database import all_models
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade(tables):
|
||||||
schema = gen_sqlalchemy_metadata(all_models)
|
|
||||||
|
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('imagestoragelocation',
|
op.create_table('imagestoragelocation',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
@ -27,7 +23,7 @@ def upgrade():
|
||||||
)
|
)
|
||||||
op.create_index('imagestoragelocation_name', 'imagestoragelocation', ['name'], unique=True)
|
op.create_index('imagestoragelocation_name', 'imagestoragelocation', ['name'], unique=True)
|
||||||
|
|
||||||
op.bulk_insert(schema.tables['imagestoragelocation'],
|
op.bulk_insert(tables.imagestoragelocation,
|
||||||
[
|
[
|
||||||
{'id':1, 'name':'s3_us_east_1'},
|
{'id':1, 'name':'s3_us_east_1'},
|
||||||
{'id':2, 'name':'s3_eu_west_1'},
|
{'id':2, 'name':'s3_eu_west_1'},
|
||||||
|
@ -52,12 +48,8 @@ def upgrade():
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index('imagestorageplacement_storage_id_location_id', table_name='imagestorageplacement')
|
|
||||||
op.drop_index('imagestorageplacement_storage_id', table_name='imagestorageplacement')
|
|
||||||
op.drop_index('imagestorageplacement_location_id', table_name='imagestorageplacement')
|
|
||||||
op.drop_table('imagestorageplacement')
|
op.drop_table('imagestorageplacement')
|
||||||
op.drop_index('imagestoragelocation_name', table_name='imagestoragelocation')
|
|
||||||
op.drop_table('imagestoragelocation')
|
op.drop_table('imagestoragelocation')
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Remove the old webhooks table.
|
||||||
|
|
||||||
|
Revision ID: f42b0ea7a4d
|
||||||
|
Revises: 4fdb65816b8d
|
||||||
|
Create Date: 2014-09-03 13:43:23.391464
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f42b0ea7a4d'
|
||||||
|
down_revision = '4fdb65816b8d'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('webhook')
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('webhook',
|
||||||
|
sa.Column('id', mysql.INTEGER(display_width=11), nullable=False),
|
||||||
|
sa.Column('public_id', mysql.VARCHAR(length=255), nullable=False),
|
||||||
|
sa.Column('repository_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('parameters', mysql.LONGTEXT(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['repository_id'], [u'repository.id'], name=u'fk_webhook_repository_repository_id'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_default_charset=u'latin1',
|
||||||
|
mysql_engine=u'InnoDB'
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
File diff suppressed because it is too large
Load diff
|
@ -46,7 +46,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
def validate_redirect_uri(self, client_id, redirect_uri):
|
def validate_redirect_uri(self, client_id, redirect_uri):
|
||||||
try:
|
try:
|
||||||
app = OAuthApplication.get(client_id=client_id)
|
app = OAuthApplication.get(client_id=client_id)
|
||||||
if app.redirect_uri and redirect_uri.startswith(app.redirect_uri):
|
if app.redirect_uri and redirect_uri and redirect_uri.startswith(app.redirect_uri):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except OAuthApplication.DoesNotExist:
|
except OAuthApplication.DoesNotExist:
|
||||||
|
|
|
@ -17,7 +17,12 @@ OPTION_TRANSLATIONS = {
|
||||||
|
|
||||||
|
|
||||||
def gen_sqlalchemy_metadata(peewee_model_list):
|
def gen_sqlalchemy_metadata(peewee_model_list):
|
||||||
metadata = MetaData()
|
metadata = MetaData(naming_convention={
|
||||||
|
"ix": 'ix_%(column_0_label)s',
|
||||||
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
|
"pk": "pk_%(table_name)s"
|
||||||
|
})
|
||||||
|
|
||||||
for model in peewee_model_list:
|
for model in peewee_model_list:
|
||||||
meta = model._meta
|
meta = model._meta
|
||||||
|
|
|
@ -68,9 +68,8 @@ class WorkQueue(object):
|
||||||
'retries_remaining': retries_remaining,
|
'retries_remaining': retries_remaining,
|
||||||
}
|
}
|
||||||
|
|
||||||
if available_after:
|
available_date = datetime.utcnow() + timedelta(seconds=available_after or 0)
|
||||||
available_date = datetime.utcnow() + timedelta(seconds=available_after)
|
params['available_after'] = available_date
|
||||||
params['available_after'] = available_date
|
|
||||||
|
|
||||||
with self._transaction_factory(db):
|
with self._transaction_factory(db):
|
||||||
QueueItem.create(**params)
|
QueueItem.create(**params)
|
||||||
|
|
|
@ -7,14 +7,14 @@ class UserEventBuilder(object):
|
||||||
Defines a helper class for constructing UserEvent and UserEventListener
|
Defines a helper class for constructing UserEvent and UserEventListener
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host):
|
def __init__(self, redis_config):
|
||||||
self._redis_host = redis_host
|
self._redis_config = redis_config
|
||||||
|
|
||||||
def get_event(self, username):
|
def get_event(self, username):
|
||||||
return UserEvent(self._redis_host, username)
|
return UserEvent(self._redis_config, username)
|
||||||
|
|
||||||
def get_listener(self, username, events):
|
def get_listener(self, username, events):
|
||||||
return UserEventListener(self._redis_host, username, events)
|
return UserEventListener(self._redis_config, username, events)
|
||||||
|
|
||||||
|
|
||||||
class UserEventsBuilderModule(object):
|
class UserEventsBuilderModule(object):
|
||||||
|
@ -26,8 +26,14 @@ class UserEventsBuilderModule(object):
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
redis_hostname = app.config.get('USER_EVENTS_REDIS_HOSTNAME')
|
redis_config = app.config.get('USER_EVENTS_REDIS')
|
||||||
user_events = UserEventBuilder(redis_hostname)
|
if not redis_config:
|
||||||
|
# This is the old key name.
|
||||||
|
redis_config = {
|
||||||
|
'host': app.config.get('USER_EVENTS_REDIS_HOSTNAME')
|
||||||
|
}
|
||||||
|
|
||||||
|
user_events = UserEventBuilder(redis_config)
|
||||||
|
|
||||||
# register extension with app
|
# register extension with app
|
||||||
app.extensions = getattr(app, 'extensions', {})
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
@ -43,8 +49,8 @@ class UserEvent(object):
|
||||||
Defines a helper class for publishing to realtime user events
|
Defines a helper class for publishing to realtime user events
|
||||||
as backed by Redis.
|
as backed by Redis.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host, username):
|
def __init__(self, redis_config, username):
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
self._username = username
|
self._username = username
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -74,10 +80,10 @@ class UserEventListener(object):
|
||||||
Defines a helper class for subscribing to realtime user events as
|
Defines a helper class for subscribing to realtime user events as
|
||||||
backed by Redis.
|
backed by Redis.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host, username, events=set([])):
|
def __init__(self, redis_config, username, events=set([])):
|
||||||
channels = [self._user_event_key(username, e) for e in events]
|
channels = [self._user_event_key(username, e) for e in events]
|
||||||
|
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
self._pubsub = self._redis.pubsub()
|
self._pubsub = self._redis.pubsub()
|
||||||
self._pubsub.subscribe(channels)
|
self._pubsub.subscribe(channels)
|
||||||
|
|
||||||
|
|
|
@ -1,110 +1,35 @@
|
||||||
import boto
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
|
||||||
import magic
|
import magic
|
||||||
|
|
||||||
from boto.s3.key import Key
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from flask import url_for, request, send_file, make_response, abort
|
from flask import url_for, request, send_file, make_response, abort
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
|
from _pyio import BufferedReader
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FakeUserfiles(object):
|
|
||||||
def prepare_for_drop(self, mime_type):
|
|
||||||
return ('http://fake/url', uuid4())
|
|
||||||
|
|
||||||
def store_file(self, file_like_obj, content_type):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_file_url(self, file_id, expires_in=300):
|
|
||||||
return ('http://fake/url')
|
|
||||||
|
|
||||||
def get_file_checksum(self, file_id):
|
|
||||||
return 'abcdefg'
|
|
||||||
|
|
||||||
|
|
||||||
class S3FileWriteException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
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 = path
|
|
||||||
self._s3_conn = None
|
|
||||||
self._bucket = None
|
|
||||||
|
|
||||||
def _initialize_s3(self):
|
|
||||||
if not self._initialized:
|
|
||||||
self._s3_conn = boto.connect_s3(self._access_key, self._secret_key)
|
|
||||||
self._bucket = self._s3_conn.get_bucket(self._bucket_name)
|
|
||||||
self._initialized = True
|
|
||||||
|
|
||||||
def prepare_for_drop(self, mime_type):
|
|
||||||
""" Returns a signed URL to upload a file to our bucket. """
|
|
||||||
self._initialize_s3()
|
|
||||||
logger.debug('Requested upload url with content type: %s' % mime_type)
|
|
||||||
file_id = str(uuid4())
|
|
||||||
full_key = os.path.join(self._prefix, file_id)
|
|
||||||
k = Key(self._bucket, full_key)
|
|
||||||
url = k.generate_url(300, 'PUT', headers={'Content-Type': mime_type},
|
|
||||||
encrypt_key=True)
|
|
||||||
return (url, file_id)
|
|
||||||
|
|
||||||
def store_file(self, file_like_obj, content_type):
|
|
||||||
self._initialize_s3()
|
|
||||||
file_id = str(uuid4())
|
|
||||||
full_key = os.path.join(self._prefix, file_id)
|
|
||||||
k = Key(self._bucket, full_key)
|
|
||||||
logger.debug('Setting s3 content type to: %s' % content_type)
|
|
||||||
k.set_metadata('Content-Type', content_type)
|
|
||||||
bytes_written = k.set_contents_from_file(file_like_obj, encrypt_key=True,
|
|
||||||
rewind=True)
|
|
||||||
|
|
||||||
if bytes_written == 0:
|
|
||||||
raise S3FileWriteException('Unable to write file to S3')
|
|
||||||
|
|
||||||
return file_id
|
|
||||||
|
|
||||||
def get_file_url(self, file_id, expires_in=300, mime_type=None):
|
|
||||||
self._initialize_s3()
|
|
||||||
full_key = os.path.join(self._prefix, file_id)
|
|
||||||
k = Key(self._bucket, full_key)
|
|
||||||
headers = None
|
|
||||||
if mime_type:
|
|
||||||
headers={'Content-Type': mime_type}
|
|
||||||
|
|
||||||
return k.generate_url(expires_in, headers=headers)
|
|
||||||
|
|
||||||
def get_file_checksum(self, file_id):
|
|
||||||
self._initialize_s3()
|
|
||||||
full_key = os.path.join(self._prefix, file_id)
|
|
||||||
k = self._bucket.lookup(full_key)
|
|
||||||
return k.etag[1:-1][:7]
|
|
||||||
|
|
||||||
|
|
||||||
class UserfilesHandlers(View):
|
class UserfilesHandlers(View):
|
||||||
methods = ['GET', 'PUT']
|
methods = ['GET', 'PUT']
|
||||||
|
|
||||||
def __init__(self, local_userfiles):
|
def __init__(self, distributed_storage, location, files):
|
||||||
self._userfiles = local_userfiles
|
self._storage = distributed_storage
|
||||||
|
self._files = files
|
||||||
|
self._locations = {location}
|
||||||
self._magic = magic.Magic(mime=True)
|
self._magic = magic.Magic(mime=True)
|
||||||
|
|
||||||
def get(self, file_id):
|
def get(self, file_id):
|
||||||
path = self._userfiles.file_path(file_id)
|
path = self._files.get_file_id_path(file_id)
|
||||||
if not os.path.exists(path):
|
try:
|
||||||
|
file_stream = self._storage.stream_read_file(self._locations, path)
|
||||||
|
buffered = BufferedReader(file_stream)
|
||||||
|
file_header_bytes = buffered.peek(1024)
|
||||||
|
return send_file(buffered, mimetype=self._magic.from_buffer(file_header_bytes))
|
||||||
|
except IOError:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
logger.debug('Sending path: %s' % path)
|
|
||||||
return send_file(path, mimetype=self._magic.from_file(path))
|
|
||||||
|
|
||||||
def put(self, file_id):
|
def put(self, file_id):
|
||||||
input_stream = request.stream
|
input_stream = request.stream
|
||||||
if request.headers.get('transfer-encoding') == 'chunked':
|
if request.headers.get('transfer-encoding') == 'chunked':
|
||||||
|
@ -112,7 +37,10 @@ class UserfilesHandlers(View):
|
||||||
# encoding (Gunicorn)
|
# encoding (Gunicorn)
|
||||||
input_stream = request.environ['wsgi.input']
|
input_stream = request.environ['wsgi.input']
|
||||||
|
|
||||||
self._userfiles.store_stream(input_stream, file_id)
|
c_type = request.headers.get('Content-Type', None)
|
||||||
|
|
||||||
|
path = self._files.get_file_id_path(file_id)
|
||||||
|
self._storage.stream_write(self._locations, path, input_stream, c_type)
|
||||||
|
|
||||||
return make_response('Okay')
|
return make_response('Okay')
|
||||||
|
|
||||||
|
@ -123,99 +51,82 @@ class UserfilesHandlers(View):
|
||||||
return self.put(file_id)
|
return self.put(file_id)
|
||||||
|
|
||||||
|
|
||||||
class LocalUserfiles(object):
|
class DelegateUserfiles(object):
|
||||||
def __init__(self, app, path):
|
def __init__(self, app, distributed_storage, location, path, handler_name):
|
||||||
self._root_path = path
|
|
||||||
self._buffer_size = 64 * 1024 # 64 KB
|
|
||||||
self._app = app
|
self._app = app
|
||||||
|
self._storage = distributed_storage
|
||||||
|
self._locations = {location}
|
||||||
|
self._prefix = path
|
||||||
|
self._handler_name = handler_name
|
||||||
|
|
||||||
def _build_url_adapter(self):
|
def _build_url_adapter(self):
|
||||||
return self._app.url_map.bind(self._app.config['SERVER_HOSTNAME'],
|
return self._app.url_map.bind(self._app.config['SERVER_HOSTNAME'],
|
||||||
script_name=self._app.config['APPLICATION_ROOT'] or '/',
|
script_name=self._app.config['APPLICATION_ROOT'] or '/',
|
||||||
url_scheme=self._app.config['PREFERRED_URL_SCHEME'])
|
url_scheme=self._app.config['PREFERRED_URL_SCHEME'])
|
||||||
|
|
||||||
def prepare_for_drop(self, mime_type):
|
def get_file_id_path(self, file_id):
|
||||||
|
return os.path.join(self._prefix, file_id)
|
||||||
|
|
||||||
|
def prepare_for_drop(self, mime_type, requires_cors=True):
|
||||||
|
""" Returns a signed URL to upload a file to our bucket. """
|
||||||
|
logger.debug('Requested upload url with content type: %s' % mime_type)
|
||||||
file_id = str(uuid4())
|
file_id = str(uuid4())
|
||||||
with self._app.app_context() as ctx:
|
path = self.get_file_id_path(file_id)
|
||||||
ctx.url_adapter = self._build_url_adapter()
|
url = self._storage.get_direct_upload_url(self._locations, path, mime_type, requires_cors)
|
||||||
return (url_for('userfiles_handlers', file_id=file_id, _external=True), file_id)
|
|
||||||
|
|
||||||
def file_path(self, file_id):
|
if url is None:
|
||||||
if '..' in file_id or file_id.startswith('/'):
|
with self._app.app_context() as ctx:
|
||||||
raise RuntimeError('Invalid Filename')
|
ctx.url_adapter = self._build_url_adapter()
|
||||||
return os.path.join(self._root_path, file_id)
|
return (url_for(self._handler_name, file_id=file_id, _external=True), file_id)
|
||||||
|
|
||||||
def store_stream(self, stream, file_id):
|
return (url, 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:
|
def store_file(self, file_like_obj, content_type, content_encoding=None, file_id=None):
|
||||||
while True:
|
if file_id is None:
|
||||||
try:
|
file_id = str(uuid4())
|
||||||
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):
|
path = self.get_file_id_path(file_id)
|
||||||
file_id = str(uuid4())
|
self._storage.stream_write(self._locations, path, file_like_obj, content_type,
|
||||||
|
content_encoding)
|
||||||
# Rewind the file to match what s3 does
|
|
||||||
file_like_obj.seek(0, os.SEEK_SET)
|
|
||||||
|
|
||||||
self.store_stream(file_like_obj, file_id)
|
|
||||||
return file_id
|
return file_id
|
||||||
|
|
||||||
def get_file_url(self, file_id, expires_in=300):
|
def get_file_url(self, file_id, expires_in=300, requires_cors=False):
|
||||||
with self._app.app_context() as ctx:
|
path = self.get_file_id_path(file_id)
|
||||||
ctx.url_adapter = self._build_url_adapter()
|
url = self._storage.get_direct_download_url(self._locations, path, expires_in, requires_cors)
|
||||||
return url_for('userfiles_handlers', file_id=file_id, _external=True)
|
|
||||||
|
if url is None:
|
||||||
|
with self._app.app_context() as ctx:
|
||||||
|
ctx.url_adapter = self._build_url_adapter()
|
||||||
|
return url_for(self._handler_name, file_id=file_id, _external=True)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
def get_file_checksum(self, file_id):
|
def get_file_checksum(self, file_id):
|
||||||
path = self.file_path(file_id)
|
path = self.get_file_id_path(file_id)
|
||||||
sha_hash = hashlib.sha256()
|
return self._storage.get_checksum(self._locations, path)
|
||||||
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):
|
class Userfiles(object):
|
||||||
def __init__(self, app=None):
|
def __init__(self, app=None, distributed_storage=None):
|
||||||
self.app = app
|
self.app = app
|
||||||
if app is not None:
|
if app is not None:
|
||||||
self.state = self.init_app(app)
|
self.state = self.init_app(app, distributed_storage)
|
||||||
else:
|
else:
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app, distributed_storage):
|
||||||
storage_type = app.config.get('USERFILES_TYPE', 'LocalUserfiles')
|
location = app.config.get('USERFILES_LOCATION')
|
||||||
path = app.config.get('USERFILES_PATH', '')
|
path = app.config.get('USERFILES_PATH', None)
|
||||||
|
|
||||||
if storage_type == 'LocalUserfiles':
|
handler_name = 'userfiles_handlers'
|
||||||
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':
|
userfiles = DelegateUserfiles(app, distributed_storage, location, path, handler_name)
|
||||||
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':
|
app.add_url_rule('/userfiles/<file_id>',
|
||||||
userfiles = FakeUserfiles()
|
view_func=UserfilesHandlers.as_view(handler_name,
|
||||||
|
distributed_storage=distributed_storage,
|
||||||
else:
|
location=location,
|
||||||
raise RuntimeError('Unknown userfiles type: %s' % storage_type)
|
files=userfiles))
|
||||||
|
|
||||||
# register extension with app
|
# register extension with app
|
||||||
app.extensions = getattr(app, 'extensions', {})
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
|
45
emails/base.html
Normal file
45
emails/base.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{ subject }}</title>
|
||||||
|
</head>
|
||||||
|
<body bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; margin: 0; padding: 0;"><style type="text/css">
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
a[class="btn"] {
|
||||||
|
display: block !important; margin-bottom: 10px !important; background-image: none !important; margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
div[class="column"] {
|
||||||
|
width: auto !important; float: none !important;
|
||||||
|
}
|
||||||
|
table.social div[class="column"] {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<table class="head-wrap" bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
<td class="header container" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 100% !important; clear: both !important; margin: 0; padding: 0;">
|
||||||
|
|
||||||
|
<div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; display: block; margin: 0; padding: 15px;">
|
||||||
|
<table bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><img src="{{ app_logo }}" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; margin: 0; padding: 0;" alt="{{ app_title }}" title="{{ app_title }}"/></td>
|
||||||
|
</tr></table></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
</tr></table><!-- /HEADER --><!-- BODY --><table class="body-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
<td class="container" bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 100% !important; clear: both !important; margin: 0; padding: 0;">
|
||||||
|
|
||||||
|
<div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; display: block; margin: 0; padding: 15px;">
|
||||||
|
<table style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr></table></div><!-- /content -->
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
</tr></table><!-- /BODY -->
|
||||||
|
</body>
|
||||||
|
</html>
|
13
emails/changeemail.html
Normal file
13
emails/changeemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>E-mail Address Change Requested</h3>
|
||||||
|
|
||||||
|
This email address was recently asked to become the new e-mail address for user {{ username | user_reference }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
To confirm this change, please click the following link:<br>
|
||||||
|
{{ app_link('confirm?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/confirmemail.html
Normal file
13
emails/confirmemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Please Confirm E-mail Address</h3>
|
||||||
|
|
||||||
|
This email address was recently used to register user {{ username | user_reference }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
To confirm this email address, please click the following link:<br>
|
||||||
|
{{ app_link('confirm?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
12
emails/emailchanged.html
Normal file
12
emails/emailchanged.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account E-mail Address Changed</h3>
|
||||||
|
|
||||||
|
The email address for user {{ username | user_reference }} has been changed from this e-mail address to {{ new_email }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If this change was not expected, please immediately log into your {{ username | admin_reference }} and reset your email address.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/passwordchanged.html
Normal file
13
emails/passwordchanged.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account Password Changed</h3>
|
||||||
|
|
||||||
|
The password for user {{ username | user_reference }} has been updated.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If this change was not expected, please immediately log into your account settings and reset your email address,
|
||||||
|
or <a href="https://quay.io/contact">contact support</a>.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/paymentfailure.html
Normal file
13
emails/paymentfailure.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Subscription Payment Failure</h3>
|
||||||
|
|
||||||
|
Your recent payment for account {{ username | user_reference }} failed, which usually results in our payments processor canceling
|
||||||
|
your subscription automatically. If you would like to continue to use {{ app_title }} without interruption,
|
||||||
|
please add a new card to {{ app_title }} and re-subscribe to your plan.<br>
|
||||||
|
<br>
|
||||||
|
You can find the card and subscription management features under your {{ username | admin_reference }}<br>
|
||||||
|
|
||||||
|
{% endblock %}
|
18
emails/recovery.html
Normal file
18
emails/recovery.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account recovery</h3>
|
||||||
|
|
||||||
|
A user at {{ app_link() }} has attempted to recover their account
|
||||||
|
using this email address.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If you made this request, please click the following link to recover your account and
|
||||||
|
change your password:
|
||||||
|
{{ app_link('recovery?code=' + token) }}
|
||||||
|
<br><br>
|
||||||
|
If you did not make this request, your account has not been compromised and the user was
|
||||||
|
not given access. Please disregard this email.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/repoauthorizeemail.html
Normal file
13
emails/repoauthorizeemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Verify e-mail to receive repository notifications</h3>
|
||||||
|
|
||||||
|
A request has been made to send <a href="http://docs.quay.io/guides/notifications.html">notifications</a> to this email address for repository {{ (namespace, repository) | repository_reference }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
To verify this email address, please click the following link:<br>
|
||||||
|
{{ app_link('authrepoemail?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
17
emails/teaminvite.html
Normal file
17
emails/teaminvite.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Invitation to join team: {{ teamname }}</h3>
|
||||||
|
|
||||||
|
{{ inviter | user_reference }} has invited you to join the team <b>{{ teamname }}</b> under organization {{ organization | user_reference }}.
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
To join the team, please click the following link:<br>
|
||||||
|
{{ app_link('confirminvite?code=' + token) }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
If you were not expecting this invitation, you can ignore this email.
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from flask import Blueprint, request, make_response, jsonify
|
from flask import Blueprint, request, make_response, jsonify, session
|
||||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||||
from flask.ext.restful.utils.cors import crossdomain
|
from flask.ext.restful.utils.cors import crossdomain
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
@ -53,11 +54,6 @@ class InvalidRequest(ApiException):
|
||||||
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
|
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
|
||||||
|
|
||||||
|
|
||||||
class InvalidResponse(ApiException):
|
|
||||||
def __init__(self, error_description, payload=None):
|
|
||||||
ApiException.__init__(self, 'invalid_response', 500, error_description, payload)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidToken(ApiException):
|
class InvalidToken(ApiException):
|
||||||
def __init__(self, error_description, payload=None):
|
def __init__(self, error_description, payload=None):
|
||||||
ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
|
ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
|
||||||
|
@ -72,6 +68,11 @@ class Unauthorized(ApiException):
|
||||||
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
|
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
|
||||||
|
|
||||||
|
|
||||||
|
class FreshLoginRequired(ApiException):
|
||||||
|
def __init__(self, payload=None):
|
||||||
|
ApiException.__init__(self, 'fresh_login_required', 401, "Requires fresh login", payload)
|
||||||
|
|
||||||
|
|
||||||
class ExceedsLicenseException(ApiException):
|
class ExceedsLicenseException(ApiException):
|
||||||
def __init__(self, payload=None):
|
def __init__(self, payload=None):
|
||||||
ApiException.__init__(self, None, 402, 'Payment Required', payload)
|
ApiException.__init__(self, None, 402, 'Payment Required', payload)
|
||||||
|
@ -93,6 +94,14 @@ def handle_api_error(error):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.app_errorhandler(model.TooManyLoginAttemptsException)
|
||||||
|
@crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
|
||||||
|
def handle_too_many_login_attempts(error):
|
||||||
|
response = make_response('Too many login attempts', 429)
|
||||||
|
response.headers['Retry-After'] = int(error.retry_after)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def resource(*urls, **kwargs):
|
def resource(*urls, **kwargs):
|
||||||
def wrapper(api_resource):
|
def wrapper(api_resource):
|
||||||
if not api_resource:
|
if not api_resource:
|
||||||
|
@ -163,7 +172,7 @@ def path_param(name, description):
|
||||||
def add_param(func):
|
def add_param(func):
|
||||||
if not func:
|
if not func:
|
||||||
return func
|
return func
|
||||||
|
|
||||||
if '__api_path_params' not in dir(func):
|
if '__api_path_params' not in dir(func):
|
||||||
func.__api_path_params = {}
|
func.__api_path_params = {}
|
||||||
func.__api_path_params[name] = {
|
func.__api_path_params[name] = {
|
||||||
|
@ -265,6 +274,26 @@ def require_user_permission(permission_class, scope=None):
|
||||||
|
|
||||||
require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER)
|
require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER)
|
||||||
require_user_admin = require_user_permission(UserAdminPermission, None)
|
require_user_admin = require_user_permission(UserAdminPermission, None)
|
||||||
|
require_fresh_user_admin = require_user_permission(UserAdminPermission, None)
|
||||||
|
|
||||||
|
def require_fresh_login(func):
|
||||||
|
@add_method_metadata('requires_fresh_login', True)
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
user = get_authenticated_user()
|
||||||
|
if not user:
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
logger.debug('Checking fresh login for user %s', user.username)
|
||||||
|
|
||||||
|
last_login = session.get('login_time', datetime.datetime.min)
|
||||||
|
valid_span = datetime.datetime.now() - datetime.timedelta(minutes=10)
|
||||||
|
|
||||||
|
if not user.password_hash or last_login >= valid_span:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
raise FreshLoginRequired()
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def require_scope(scope_object):
|
def require_scope(scope_object):
|
||||||
|
@ -292,25 +321,6 @@ def validate_json_request(schema_name):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def define_json_response(schema_name):
|
|
||||||
def wrapper(func):
|
|
||||||
@add_method_metadata('response_schema', schema_name)
|
|
||||||
@wraps(func)
|
|
||||||
def wrapped(self, *args, **kwargs):
|
|
||||||
schema = self.schemas[schema_name]
|
|
||||||
try:
|
|
||||||
resp = func(self, *args, **kwargs)
|
|
||||||
|
|
||||||
if app.config['TESTING']:
|
|
||||||
validate(resp, schema)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
except ValidationError as ex:
|
|
||||||
raise InvalidResponse(ex.message)
|
|
||||||
return wrapped
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def request_error(exception=None, **kwargs):
|
def request_error(exception=None, **kwargs):
|
||||||
data = kwargs.copy()
|
data = kwargs.copy()
|
||||||
message = 'Request error.'
|
message = 'Request error.'
|
||||||
|
@ -338,6 +348,25 @@ def log_action(kind, user_or_orgname, metadata=None, repo=None):
|
||||||
metadata=metadata, repository=repo)
|
metadata=metadata, repository=repo)
|
||||||
|
|
||||||
|
|
||||||
|
def define_json_response(schema_name):
|
||||||
|
def wrapper(func):
|
||||||
|
@add_method_metadata('response_schema', schema_name)
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(self, *args, **kwargs):
|
||||||
|
schema = self.schemas[schema_name]
|
||||||
|
resp = func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
if app.config['TESTING']:
|
||||||
|
try:
|
||||||
|
validate(resp, schema)
|
||||||
|
except ValidationError as ex:
|
||||||
|
raise InvalidResponse(ex.message)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
return wrapped
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
import endpoints.api.billing
|
import endpoints.api.billing
|
||||||
import endpoints.api.build
|
import endpoints.api.build
|
||||||
import endpoints.api.discovery
|
import endpoints.api.discovery
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import request
|
||||||
from app import billing
|
from app import billing
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||||
require_user_admin, show_if, hide_if, path_param, require_scope)
|
require_user_admin, show_if, hide_if, path_param, require_scope, abort)
|
||||||
from endpoints.api.subscribe import subscribe, subscription_view
|
from endpoints.api.subscribe import subscribe, subscription_view
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -24,7 +24,11 @@ def get_card(user):
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
try:
|
||||||
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
abort(503, message='Cannot contact Stripe')
|
||||||
|
|
||||||
if cus and cus.default_card:
|
if cus and cus.default_card:
|
||||||
# Find the default card.
|
# Find the default card.
|
||||||
default_card = None
|
default_card = None
|
||||||
|
@ -47,7 +51,11 @@ def get_card(user):
|
||||||
|
|
||||||
def set_card(user, token):
|
def set_card(user, token):
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
try:
|
||||||
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
abort(503, message='Cannot contact Stripe')
|
||||||
|
|
||||||
if cus:
|
if cus:
|
||||||
try:
|
try:
|
||||||
cus.card = token
|
cus.card = token
|
||||||
|
@ -56,6 +64,8 @@ def set_card(user, token):
|
||||||
return carderror_response(exc)
|
return carderror_response(exc)
|
||||||
except stripe.InvalidRequestError as exc:
|
except stripe.InvalidRequestError as exc:
|
||||||
return carderror_response(exc)
|
return carderror_response(exc)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
return carderror_response(e)
|
||||||
|
|
||||||
return get_card(user)
|
return get_card(user)
|
||||||
|
|
||||||
|
@ -76,7 +86,11 @@ def get_invoices(customer_id):
|
||||||
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||||
}
|
}
|
||||||
|
|
||||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
try:
|
||||||
|
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
abort(503, message='Cannot contact Stripe')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'invoices': [invoice_view(i) for i in invoices.data]
|
'invoices': [invoice_view(i) for i in invoices.data]
|
||||||
}
|
}
|
||||||
|
@ -231,7 +245,10 @@ class UserPlan(ApiResource):
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
try:
|
||||||
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
abort(503, message='Cannot contact Stripe')
|
||||||
|
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
return subscription_view(cus.subscription, private_repos)
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
@ -297,7 +314,10 @@ class OrganizationPlan(ApiResource):
|
||||||
private_repos = model.get_private_repo_count(orgname)
|
private_repos = model.get_private_repo_count(orgname)
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
if organization.stripe_id:
|
if organization.stripe_id:
|
||||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
try:
|
||||||
|
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
abort(503, message='Cannot contact Stripe')
|
||||||
|
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
return subscription_view(cus.subscription, private_repos)
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import request
|
from flask import request, redirect
|
||||||
|
|
||||||
from app import app, userfiles as user_files, build_logs
|
from app import app, userfiles as user_files, build_logs, log_archive
|
||||||
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||||
require_repo_read, require_repo_write, validate_json_request,
|
require_repo_read, require_repo_write, validate_json_request,
|
||||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
|
ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
|
||||||
|
@ -81,7 +81,7 @@ def build_status_view(build_obj, can_write=False):
|
||||||
}
|
}
|
||||||
|
|
||||||
if can_write:
|
if can_write:
|
||||||
resp['archive_url'] = user_files.get_file_url(build_obj.resource_key)
|
resp['archive_url'] = user_files.get_file_url(build_obj.resource_key, requires_cors=True)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
# was used.
|
# was used.
|
||||||
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
||||||
if associated_repository:
|
if associated_repository:
|
||||||
if not ModifyRepositoryPermission(associated_repository.namespace,
|
if not ModifyRepositoryPermission(associated_repository.namespace_user.username,
|
||||||
associated_repository.name):
|
associated_repository.name):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@ -221,6 +221,10 @@ class RepositoryBuildLogs(RepositoryParamResource):
|
||||||
|
|
||||||
build = model.get_repository_build(namespace, repository, build_uuid)
|
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||||
|
|
||||||
|
# If the logs have been archived, just redirect to the completed archive
|
||||||
|
if build.logs_archived:
|
||||||
|
return redirect(log_archive.get_file_url(build.uuid))
|
||||||
|
|
||||||
start = int(request.args.get('start', 0))
|
start = int(request.args.get('start', 0))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -263,7 +267,7 @@ class FileDropResource(ApiResource):
|
||||||
def post(self):
|
def post(self):
|
||||||
""" Request a URL to which a file may be uploaded. """
|
""" Request a URL to which a file may be uploaded. """
|
||||||
mime_type = request.get_json()['mimeType']
|
mime_type = request.get_json()['mimeType']
|
||||||
(url, file_id) = user_files.prepare_for_drop(mime_type)
|
(url, file_id) = user_files.prepare_for_drop(mime_type, requires_cors=True)
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
'file_id': str(file_id),
|
'file_id': str(file_id),
|
||||||
|
|
|
@ -125,8 +125,17 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
if internal is not None:
|
if internal is not None:
|
||||||
new_operation['internal'] = True
|
new_operation['internal'] = True
|
||||||
|
|
||||||
|
if include_internal:
|
||||||
|
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
|
||||||
|
if requires_fresh_login is not None:
|
||||||
|
new_operation['requires_fresh_login'] = True
|
||||||
|
|
||||||
if not internal or (internal and include_internal):
|
if not internal or (internal and include_internal):
|
||||||
operations.append(new_operation)
|
# Swagger requires valid nicknames on all operations.
|
||||||
|
if new_operation.get('nickname'):
|
||||||
|
operations.append(new_operation)
|
||||||
|
else:
|
||||||
|
logger.debug('Operation missing nickname: %s' % method)
|
||||||
|
|
||||||
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
||||||
new_resource = {
|
new_resource = {
|
||||||
|
|
|
@ -9,22 +9,33 @@ from data import model
|
||||||
from util.cache import cache_control_flask_restful
|
from util.cache import cache_control_flask_restful
|
||||||
|
|
||||||
|
|
||||||
def image_view(image):
|
def image_view(image, image_map):
|
||||||
extended_props = image
|
extended_props = image
|
||||||
if image.storage and image.storage.id:
|
if image.storage and image.storage.id:
|
||||||
extended_props = image.storage
|
extended_props = image.storage
|
||||||
|
|
||||||
command = extended_props.command
|
command = extended_props.command
|
||||||
|
|
||||||
|
def docker_id(aid):
|
||||||
|
if not aid:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return image_map[aid]
|
||||||
|
|
||||||
|
# Calculate the ancestors string, with the DBID's replaced with the docker IDs.
|
||||||
|
ancestors = [docker_id(a) for a in image.ancestors.split('/')]
|
||||||
|
ancestors_string = '/'.join(ancestors)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': image.docker_image_id,
|
'id': image.docker_image_id,
|
||||||
'created': format_date(extended_props.created),
|
'created': format_date(extended_props.created),
|
||||||
'comment': extended_props.comment,
|
'comment': extended_props.comment,
|
||||||
'command': json.loads(command) if command else None,
|
'command': json.loads(command) if command else None,
|
||||||
'ancestors': image.ancestors,
|
|
||||||
'dbid': image.id,
|
|
||||||
'size': extended_props.image_size,
|
'size': extended_props.image_size,
|
||||||
'locations': list(image.storage.locations),
|
'locations': list(image.storage.locations),
|
||||||
'uploading': image.storage.uploading,
|
'uploading': image.storage.uploading,
|
||||||
|
'ancestors': ancestors_string,
|
||||||
|
'sort_index': len(image.ancestors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,14 +54,16 @@ class RepositoryImageList(RepositoryParamResource):
|
||||||
for tag in all_tags:
|
for tag in all_tags:
|
||||||
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
||||||
|
|
||||||
|
image_map = {}
|
||||||
|
for image in all_images:
|
||||||
|
image_map[str(image.id)] = image.docker_image_id
|
||||||
|
|
||||||
def add_tags(image_json):
|
def add_tags(image_json):
|
||||||
image_json['tags'] = tags_by_image_id[image_json['id']]
|
image_json['tags'] = tags_by_image_id[image_json['id']]
|
||||||
return image_json
|
return image_json
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'images': [add_tags(image_view(image)) for image in all_images]
|
'images': [add_tags(image_view(image, image_map)) for image in all_images]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,7 +80,12 @@ class RepositoryImage(RepositoryParamResource):
|
||||||
if not image:
|
if not image:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
return image_view(image)
|
# Lookup all the ancestor images for the image.
|
||||||
|
image_map = {}
|
||||||
|
for current_image in model.get_parent_images(namespace, repository, image):
|
||||||
|
image_map[str(current_image.id)] = image.docker_image_id
|
||||||
|
|
||||||
|
return image_view(image, image_map)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import request, abort
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
log_action, validate_json_request, NotFound, internal_only,
|
log_action, validate_json_request, NotFound, internal_only,
|
||||||
path_param)
|
path_param, show_if)
|
||||||
|
|
||||||
from app import tf
|
from app import tf
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -20,12 +20,13 @@ def record_view(record):
|
||||||
return {
|
return {
|
||||||
'email': record.email,
|
'email': record.email,
|
||||||
'repository': record.repository.name,
|
'repository': record.repository.name,
|
||||||
'namespace': record.repository.namespace,
|
'namespace': record.repository.namespace_user.username,
|
||||||
'confirmed': record.confirmed
|
'confirmed': record.confirmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.MAILING)
|
||||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('email', 'The e-mail address')
|
@path_param('email', 'The e-mail address')
|
||||||
|
|
|
@ -82,8 +82,7 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
visibility = req['visibility']
|
visibility = req['visibility']
|
||||||
|
|
||||||
repo = model.create_repository(namespace_name, repository_name, owner,
|
repo = model.create_repository(namespace_name, repository_name, owner, visibility)
|
||||||
visibility)
|
|
||||||
repo.description = req['description']
|
repo.description = req['description']
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
|
@ -112,7 +111,7 @@ class RepositoryList(ApiResource):
|
||||||
"""Fetch the list of repositories under a variety of situations."""
|
"""Fetch the list of repositories under a variety of situations."""
|
||||||
def repo_view(repo_obj):
|
def repo_view(repo_obj):
|
||||||
return {
|
return {
|
||||||
'namespace': repo_obj.namespace,
|
'namespace': repo_obj.namespace_user.username,
|
||||||
'name': repo_obj.name,
|
'name': repo_obj.name,
|
||||||
'description': repo_obj.description,
|
'description': repo_obj.description,
|
||||||
'is_public': repo_obj.visibility.name == 'public',
|
'is_public': repo_obj.visibility.name == 'public',
|
||||||
|
@ -136,7 +135,8 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
response['repositories'] = [repo_view(repo) for repo in repo_query
|
response['repositories'] = [repo_view(repo) for repo in repo_query
|
||||||
if (repo.visibility.name == 'public' or
|
if (repo.visibility.name == 'public' or
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
ReadRepositoryPermission(repo.namespace_user.username,
|
||||||
|
repo.name).can())]
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -171,8 +171,7 @@ class Repository(RepositoryParamResource):
|
||||||
def tag_view(tag):
|
def tag_view(tag):
|
||||||
return {
|
return {
|
||||||
'name': tag.name,
|
'name': tag.name,
|
||||||
'image_id': tag.image.docker_image_id,
|
'image_id': tag.image.docker_image_id
|
||||||
'dbid': tag.image.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
organization = None
|
organization = None
|
||||||
|
|
|
@ -35,6 +35,14 @@ class UserRobotList(ApiResource):
|
||||||
@internal_only
|
@internal_only
|
||||||
class UserRobot(ApiResource):
|
class UserRobot(ApiResource):
|
||||||
""" Resource for managing a user's robots. """
|
""" Resource for managing a user's robots. """
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('getUserRobot')
|
||||||
|
def get(self, robot_shortname):
|
||||||
|
""" Returns the user's robot with the specified name. """
|
||||||
|
parent = get_authenticated_user()
|
||||||
|
robot, password = model.get_robot(robot_shortname, parent)
|
||||||
|
return robot_view(robot.username, password)
|
||||||
|
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
@nickname('createUserRobot')
|
@nickname('createUserRobot')
|
||||||
def put(self, robot_shortname):
|
def put(self, robot_shortname):
|
||||||
|
@ -79,6 +87,18 @@ class OrgRobotList(ApiResource):
|
||||||
@related_user_resource(UserRobot)
|
@related_user_resource(UserRobot)
|
||||||
class OrgRobot(ApiResource):
|
class OrgRobot(ApiResource):
|
||||||
""" Resource for managing an organization's robots. """
|
""" Resource for managing an organization's robots. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('getOrgRobot')
|
||||||
|
def get(self, orgname, robot_shortname):
|
||||||
|
""" Returns the organization's robot with the specified name. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
parent = model.get_organization(orgname)
|
||||||
|
robot, password = model.get_robot(robot_shortname, parent)
|
||||||
|
return robot_view(robot.username, password)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('createOrgRobot')
|
@nickname('createOrgRobot')
|
||||||
def put(self, orgname, robot_shortname):
|
def put(self, orgname, robot_shortname):
|
||||||
|
@ -103,3 +123,38 @@ class OrgRobot(ApiResource):
|
||||||
return 'Deleted', 204
|
return 'Deleted', 204
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/robots/<robot_shortname>/regenerate')
|
||||||
|
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||||
|
@internal_only
|
||||||
|
class RegenerateUserRobot(ApiResource):
|
||||||
|
""" Resource for regenerate an organization's robot's token. """
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('regenerateUserRobotToken')
|
||||||
|
def post(self, robot_shortname):
|
||||||
|
""" Regenerates the token for a user's robot. """
|
||||||
|
parent = get_authenticated_user()
|
||||||
|
robot, password = model.regenerate_robot_token(robot_shortname, parent)
|
||||||
|
log_action('regenerate_robot_token', parent.username, {'robot': robot_shortname})
|
||||||
|
return robot_view(robot.username, password)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/regenerate')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||||
|
@related_user_resource(RegenerateUserRobot)
|
||||||
|
class RegenerateOrgRobot(ApiResource):
|
||||||
|
""" Resource for regenerate an organization's robot's token. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('regenerateOrgRobotToken')
|
||||||
|
def post(self, orgname, robot_shortname):
|
||||||
|
""" Regenerates the token for an organization robot. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
parent = model.get_organization(orgname)
|
||||||
|
robot, password = model.regenerate_robot_token(robot_shortname, parent)
|
||||||
|
log_action('regenerate_robot_token', orgname, {'robot': robot_shortname})
|
||||||
|
return robot_view(robot.username, password)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
|
@ -112,7 +112,7 @@ class FindRepositories(ApiResource):
|
||||||
|
|
||||||
def repo_view(repo):
|
def repo_view(repo):
|
||||||
return {
|
return {
|
||||||
'namespace': repo.namespace,
|
'namespace': repo.namespace_user.username,
|
||||||
'name': repo.name,
|
'name': repo.name,
|
||||||
'description': repo.description
|
'description': repo.description
|
||||||
}
|
}
|
||||||
|
@ -126,5 +126,5 @@ class FindRepositories(ApiResource):
|
||||||
return {
|
return {
|
||||||
'repositories': [repo_view(repo) for repo in matching
|
'repositories': [repo_view(repo) for repo in matching
|
||||||
if (repo.visibility.name == 'public' or
|
if (repo.visibility.name == 'public' or
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ logger = logging.getLogger(__name__)
|
||||||
def carderror_response(exc):
|
def carderror_response(exc):
|
||||||
return {'carderror': exc.message}, 402
|
return {'carderror': exc.message}, 402
|
||||||
|
|
||||||
|
def connection_response(exc):
|
||||||
|
return {'message': 'Could not contact Stripe. Please try again.'}, 503
|
||||||
|
|
||||||
|
|
||||||
def subscription_view(stripe_subscription, used_repos):
|
def subscription_view(stripe_subscription, used_repos):
|
||||||
view = {
|
view = {
|
||||||
|
@ -74,19 +77,29 @@ def subscribe(user, plan, token, require_business_plan):
|
||||||
log_action('account_change_plan', user.username, {'plan': plan})
|
log_action('account_change_plan', user.username, {'plan': plan})
|
||||||
except stripe.CardError as e:
|
except stripe.CardError as e:
|
||||||
return carderror_response(e)
|
return carderror_response(e)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
response_json = subscription_view(cus.subscription, private_repos)
|
||||||
status_code = 201
|
status_code = 201
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Change the plan
|
# Change the plan
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
try:
|
||||||
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
if plan_found['price'] == 0:
|
if plan_found['price'] == 0:
|
||||||
if cus.subscription is not None:
|
if cus.subscription is not None:
|
||||||
# We only have to cancel the subscription if they actually have one
|
# We only have to cancel the subscription if they actually have one
|
||||||
cus.cancel_subscription()
|
try:
|
||||||
cus.save()
|
cus.cancel_subscription()
|
||||||
|
cus.save()
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
|
|
||||||
check_repository_usage(user, plan_found)
|
check_repository_usage(user, plan_found)
|
||||||
log_action('account_change_plan', user.username, {'plan': plan})
|
log_action('account_change_plan', user.username, {'plan': plan})
|
||||||
|
|
||||||
|
@ -101,6 +114,8 @@ def subscribe(user, plan, token, require_business_plan):
|
||||||
cus.save()
|
cus.save()
|
||||||
except stripe.CardError as e:
|
except stripe.CardError as e:
|
||||||
return carderror_response(e)
|
return carderror_response(e)
|
||||||
|
except stripe.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
response_json = subscription_view(cus.subscription, private_repos)
|
||||||
check_repository_usage(user, plan_found)
|
check_repository_usage(user, plan_found)
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
|
import string
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from random import SystemRandom
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin, format_date,
|
log_action, internal_only, NotFound, require_user_admin, format_date,
|
||||||
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
||||||
query_param, abort, path_param)
|
query_param, abort, require_fresh_login, path_param)
|
||||||
|
|
||||||
from endpoints.api.logs import get_logs
|
from endpoints.api.logs import get_logs
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from util.useremails import send_confirmation_email, send_recovery_email
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -42,24 +44,6 @@ class SuperUserLogs(ApiResource):
|
||||||
abort(403)
|
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_USER_LIMIT', 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
|
|
||||||
def user_view(user):
|
def user_view(user):
|
||||||
return {
|
return {
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
|
@ -73,6 +57,26 @@ def user_view(user):
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
class SuperUserList(ApiResource):
|
class SuperUserList(ApiResource):
|
||||||
""" Resource for listing users in the system. """
|
""" Resource for listing users in the system. """
|
||||||
|
schemas = {
|
||||||
|
'CreateInstallUser': {
|
||||||
|
'id': 'CreateInstallUser',
|
||||||
|
'description': 'Data for creating a user',
|
||||||
|
'required': ['username', 'email'],
|
||||||
|
'properties': {
|
||||||
|
'username': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The username of the user being created'
|
||||||
|
},
|
||||||
|
|
||||||
|
'email': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The email address of the user being created'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('listAllUsers')
|
@nickname('listAllUsers')
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns a list of all users in the system. """
|
""" Returns a list of all users in the system. """
|
||||||
|
@ -85,6 +89,63 @@ class SuperUserList(ApiResource):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
|
@nickname('createInstallUser')
|
||||||
|
@validate_json_request('CreateInstallUser')
|
||||||
|
def post(self):
|
||||||
|
""" Creates a new user. """
|
||||||
|
user_information = request.get_json()
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
username = user_information['username']
|
||||||
|
email = user_information['email']
|
||||||
|
|
||||||
|
# Generate a temporary password for the user.
|
||||||
|
random = SystemRandom()
|
||||||
|
password = ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(32)])
|
||||||
|
|
||||||
|
# Create the user.
|
||||||
|
user = model.create_user(username, password, email, auto_verify=not features.MAILING)
|
||||||
|
|
||||||
|
# If mailing is turned on, send the user a verification email.
|
||||||
|
if features.MAILING:
|
||||||
|
confirmation = model.create_confirm_email_code(user, new_email=user.email)
|
||||||
|
send_confirmation_email(user.username, user.email, confirmation.code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'email': email,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/superusers/users/<username>/sendrecovery')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class SuperUserSendRecoveryEmail(ApiResource):
|
||||||
|
""" Resource for sending a recovery user on behalf of a user. """
|
||||||
|
@require_fresh_login
|
||||||
|
@nickname('sendInstallUserRecoveryEmail')
|
||||||
|
def post(self, username):
|
||||||
|
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)
|
||||||
|
|
||||||
|
code = model.create_reset_password_email_code(user.email)
|
||||||
|
send_recovery_email(user.email, code.code)
|
||||||
|
return {
|
||||||
|
'email': user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/superuser/users/<username>')
|
@resource('/v1/superuser/users/<username>')
|
||||||
@path_param('username', 'The username of the user being managed')
|
@path_param('username', 'The username of the user being managed')
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@ -109,18 +170,20 @@ class SuperUserManagement(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('getInstallUser')
|
@nickname('getInstallUser')
|
||||||
def get(self, username):
|
def get(self, username):
|
||||||
""" Returns information about the specified user. """
|
""" Returns information about the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
user = model.get_user(username)
|
user = model.get_user(username)
|
||||||
if not user or user.organization or user.robot:
|
if not user or user.organization or user.robot:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('deleteInstallUser')
|
@nickname('deleteInstallUser')
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
""" Deletes the specified user. """
|
""" Deletes the specified user. """
|
||||||
|
@ -137,6 +200,7 @@ class SuperUserManagement(ApiResource):
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('changeInstallUser')
|
@nickname('changeInstallUser')
|
||||||
@validate_json_request('UpdateUser')
|
@validate_json_request('UpdateUser')
|
||||||
def put(self, username):
|
def put(self, username):
|
||||||
|
|
|
@ -90,11 +90,14 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
parent_images = model.get_parent_images(namespace, repository, tag_image)
|
parent_images = model.get_parent_images(namespace, repository, tag_image)
|
||||||
|
image_map = {}
|
||||||
|
for image in parent_images:
|
||||||
|
image_map[str(image.id)] = image.docker_image_id
|
||||||
|
|
||||||
parents = list(parent_images)
|
parents = list(parent_images)
|
||||||
parents.reverse()
|
parents.reverse()
|
||||||
all_images = [tag_image] + parents
|
all_images = [tag_image] + parents
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'images': [image_view(image) for image in all_images]
|
'images': [image_view(image, image_map) for image in all_images]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,51 @@ from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||||
path_param)
|
path_param, query_param, truthy_bool, parse_args, require_user_admin,
|
||||||
|
show_if)
|
||||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
|
from util.useremails import send_org_invite_email
|
||||||
|
from util.gravatar import compute_hash
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
|
def try_accept_invite(code, user):
|
||||||
|
(team, inviter) = model.confirm_team_invite(code, user)
|
||||||
|
|
||||||
|
model.delete_matching_notifications(user, 'org_team_invite', code=code)
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
log_action('org_team_member_invite_accepted', orgname, {
|
||||||
|
'member': user.username,
|
||||||
|
'team': team.name,
|
||||||
|
'inviter': inviter.username
|
||||||
|
})
|
||||||
|
|
||||||
|
return team
|
||||||
|
|
||||||
|
|
||||||
|
def handle_addinvite_team(inviter, team, user=None, email=None):
|
||||||
|
invite = model.add_or_invite_to_team(inviter, team, user, email,
|
||||||
|
requires_invite = features.MAILING)
|
||||||
|
if not invite:
|
||||||
|
# User was added to the team directly.
|
||||||
|
return
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
if user:
|
||||||
|
model.create_notification('org_team_invite', user, metadata = {
|
||||||
|
'code': invite.invite_token,
|
||||||
|
'inviter': inviter.username,
|
||||||
|
'org': orgname,
|
||||||
|
'team': team.name
|
||||||
|
})
|
||||||
|
|
||||||
|
send_org_invite_email(user.username if user else email, user.email if user else email,
|
||||||
|
orgname, team.name, inviter.username, invite.invite_token)
|
||||||
|
return invite
|
||||||
|
|
||||||
def team_view(orgname, team):
|
def team_view(orgname, team):
|
||||||
view_permission = ViewTeamPermission(orgname, team.name)
|
view_permission = ViewTeamPermission(orgname, team.name)
|
||||||
|
@ -20,14 +59,28 @@ def team_view(orgname, team):
|
||||||
'role': role
|
'role': role
|
||||||
}
|
}
|
||||||
|
|
||||||
def member_view(member):
|
def member_view(member, invited=False):
|
||||||
return {
|
return {
|
||||||
'name': member.username,
|
'name': member.username,
|
||||||
'kind': 'user',
|
'kind': 'user',
|
||||||
'is_robot': member.robot,
|
'is_robot': member.robot,
|
||||||
|
'gravatar': compute_hash(member.email) if not member.robot else None,
|
||||||
|
'invited': invited,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def invite_view(invite):
|
||||||
|
if invite.user:
|
||||||
|
return member_view(invite.user, invited=True)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'email': invite.email,
|
||||||
|
'kind': 'invite',
|
||||||
|
'gravatar': compute_hash(invite.email),
|
||||||
|
'invited': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/team/<teamname>')
|
@resource('/v1/organization/<orgname>/team/<teamname>')
|
||||||
@path_param('orgname', 'The name of the organization')
|
@path_param('orgname', 'The name of the organization')
|
||||||
@path_param('teamname', 'The name of the team')
|
@path_param('teamname', 'The name of the team')
|
||||||
|
@ -119,10 +172,11 @@ class OrganizationTeam(ApiResource):
|
||||||
@path_param('teamname', 'The name of the team')
|
@path_param('teamname', 'The name of the team')
|
||||||
class TeamMemberList(ApiResource):
|
class TeamMemberList(ApiResource):
|
||||||
""" Resource for managing the list of members for a team. """
|
""" Resource for managing the list of members for a team. """
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@parse_args
|
||||||
|
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
||||||
@nickname('getOrganizationTeamMembers')
|
@nickname('getOrganizationTeamMembers')
|
||||||
def get(self, orgname, teamname):
|
def get(self, args, orgname, teamname):
|
||||||
""" Retrieve the list of members for the specified team. """
|
""" Retrieve the list of members for the specified team. """
|
||||||
view_permission = ViewTeamPermission(orgname, teamname)
|
view_permission = ViewTeamPermission(orgname, teamname)
|
||||||
edit_permission = AdministerOrganizationPermission(orgname)
|
edit_permission = AdministerOrganizationPermission(orgname)
|
||||||
|
@ -135,11 +189,18 @@ class TeamMemberList(ApiResource):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
members = model.get_organization_team_members(team.id)
|
members = model.get_organization_team_members(team.id)
|
||||||
return {
|
invites = []
|
||||||
'members': {m.username : member_view(m) for m in members},
|
|
||||||
|
if args['includePending'] and edit_permission.can():
|
||||||
|
invites = model.get_organization_team_member_invites(team.id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'members': [member_view(m) for m in members] + [invite_view(i) for i in invites],
|
||||||
'can_edit': edit_permission.can()
|
'can_edit': edit_permission.can()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@ -153,7 +214,7 @@ class TeamMember(ApiResource):
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationTeamMember')
|
@nickname('updateOrganizationTeamMember')
|
||||||
def put(self, orgname, teamname, membername):
|
def put(self, orgname, teamname, membername):
|
||||||
""" Add a member to an existing team. """
|
""" Adds or invites a member to an existing team. """
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
team = None
|
team = None
|
||||||
|
@ -170,23 +231,151 @@ class TeamMember(ApiResource):
|
||||||
if not user:
|
if not user:
|
||||||
raise request_error(message='Unknown user')
|
raise request_error(message='Unknown user')
|
||||||
|
|
||||||
# Add the user to the team.
|
# Add or invite the user to the team.
|
||||||
model.add_user_to_team(user, team)
|
inviter = get_authenticated_user()
|
||||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
invite = handle_addinvite_team(inviter, team, user=user)
|
||||||
return member_view(user)
|
if not invite:
|
||||||
|
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||||
|
return member_view(user, invited=False)
|
||||||
|
|
||||||
|
# User was invited.
|
||||||
|
log_action('org_invite_team_member', orgname, {
|
||||||
|
'user': membername,
|
||||||
|
'member': membername,
|
||||||
|
'team': teamname
|
||||||
|
})
|
||||||
|
return member_view(user, invited=True)
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('deleteOrganizationTeamMember')
|
@nickname('deleteOrganizationTeamMember')
|
||||||
def delete(self, orgname, teamname, membername):
|
def delete(self, orgname, teamname, membername):
|
||||||
""" Delete an existing member of a team. """
|
""" Delete a member of a team. If the user is merely invited to join
|
||||||
|
the team, then the invite is removed instead.
|
||||||
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
# Remote the user from the team.
|
# Remote the user from the team.
|
||||||
invoking_user = get_authenticated_user().username
|
invoking_user = get_authenticated_user().username
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Find the member.
|
||||||
|
member = model.get_user(membername)
|
||||||
|
if not member:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# First attempt to delete an invite for the user to this team. If none found,
|
||||||
|
# then we try to remove the user directly.
|
||||||
|
if model.delete_team_user_invite(team, member):
|
||||||
|
log_action('org_delete_team_member_invite', orgname, {
|
||||||
|
'user': membername,
|
||||||
|
'team': teamname,
|
||||||
|
'member': membername
|
||||||
|
})
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
||||||
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
||||||
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
||||||
return 'Deleted', 204
|
return 'Deleted', 204
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/team/<teamname>/invite/<email>')
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class InviteTeamMember(ApiResource):
|
||||||
|
""" Resource for inviting a team member via email address. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('inviteTeamMemberEmail')
|
||||||
|
def put(self, orgname, teamname, email):
|
||||||
|
""" Invites an email address to an existing team. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
team = None
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Invite the email to the team.
|
||||||
|
inviter = get_authenticated_user()
|
||||||
|
invite = handle_addinvite_team(inviter, team, email=email)
|
||||||
|
log_action('org_invite_team_member', orgname, {
|
||||||
|
'email': email,
|
||||||
|
'team': teamname,
|
||||||
|
'member': email
|
||||||
|
})
|
||||||
|
return invite_view(invite)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('deleteTeamMemberEmailInvite')
|
||||||
|
def delete(self, orgname, teamname, email):
|
||||||
|
""" Delete an invite of an email address to join a team. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
team = None
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Delete the invite.
|
||||||
|
model.delete_team_email_invite(team, email)
|
||||||
|
log_action('org_delete_team_member_invite', orgname, {
|
||||||
|
'email': email,
|
||||||
|
'team': teamname,
|
||||||
|
'member': email
|
||||||
|
})
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/teaminvite/<code>')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class TeamMemberInvite(ApiResource):
|
||||||
|
""" Resource for managing invites to jon a team. """
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('acceptOrganizationTeamInvite')
|
||||||
|
def put(self, code):
|
||||||
|
""" Accepts an invite to join a team in an organization. """
|
||||||
|
# Accept the invite for the current user.
|
||||||
|
team = try_accept_invite(code, get_authenticated_user())
|
||||||
|
if not team:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
return {
|
||||||
|
'org': orgname,
|
||||||
|
'team': team.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('declineOrganizationTeamInvite')
|
||||||
|
@require_user_admin
|
||||||
|
def delete(self, code):
|
||||||
|
""" Delete an existing member of a team. """
|
||||||
|
(team, inviter) = model.delete_team_invite(code, get_authenticated_user())
|
||||||
|
|
||||||
|
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
log_action('org_team_member_invite_declined', orgname, {
|
||||||
|
'member': get_authenticated_user().username,
|
||||||
|
'team': team.name,
|
||||||
|
'inviter': inviter.username
|
||||||
|
})
|
||||||
|
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
|
@ -15,7 +15,7 @@ from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuil
|
||||||
from endpoints.common import start_build
|
from endpoints.common import start_build
|
||||||
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
|
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
|
||||||
TriggerActivationException, EmptyRepositoryException,
|
TriggerActivationException, EmptyRepositoryException,
|
||||||
RepositoryReadException)
|
RepositoryReadException, TriggerStartException)
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
||||||
from util.names import parse_robot_username
|
from util.names import parse_robot_username
|
||||||
|
@ -212,7 +212,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
'write')
|
'write')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repository_path = '%s/%s' % (trigger.repository.namespace,
|
repository_path = '%s/%s' % (trigger.repository.namespace_user.username,
|
||||||
trigger.repository.name)
|
trigger.repository.name)
|
||||||
path = url_for('webhooks.build_trigger_webhook',
|
path = url_for('webhooks.build_trigger_webhook',
|
||||||
repository=repository_path, trigger_uuid=trigger.uuid)
|
repository=repository_path, trigger_uuid=trigger.uuid)
|
||||||
|
@ -385,9 +385,24 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
class ActivateBuildTrigger(RepositoryParamResource):
|
class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
""" Custom verb to manually activate a build trigger. """
|
""" Custom verb to manually activate a build trigger. """
|
||||||
|
schemas = {
|
||||||
|
'RunParameters': {
|
||||||
|
'id': 'RunParameters',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Optional run parameters for activating the build trigger',
|
||||||
|
'additional_properties': False,
|
||||||
|
'properties': {
|
||||||
|
'branch_name': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '(GitHub Only) If specified, the name of the GitHub branch to build.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('manuallyStartBuildTrigger')
|
@nickname('manuallyStartBuildTrigger')
|
||||||
|
@validate_json_request('RunParameters')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace, repository, trigger_uuid):
|
||||||
""" Manually start a build from the specified trigger. """
|
""" Manually start a build from the specified trigger. """
|
||||||
try:
|
try:
|
||||||
|
@ -400,14 +415,18 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
if not handler.is_active(config_dict):
|
if not handler.is_active(config_dict):
|
||||||
raise InvalidRequest('Trigger is not active.')
|
raise InvalidRequest('Trigger is not active.')
|
||||||
|
|
||||||
specs = handler.manual_start(trigger.auth_token, config_dict)
|
try:
|
||||||
dockerfile_id, tags, name, subdir = specs
|
run_parameters = request.get_json()
|
||||||
|
specs = handler.manual_start(trigger.auth_token, config_dict, run_parameters=run_parameters)
|
||||||
|
dockerfile_id, tags, name, subdir = specs
|
||||||
|
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
pull_robot_name = model.get_pull_robot_name(trigger)
|
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)
|
pull_robot_name=pull_robot_name)
|
||||||
|
except TriggerStartException as tse:
|
||||||
|
raise InvalidRequest(tse.message)
|
||||||
|
|
||||||
resp = build_status_view(build_request, True)
|
resp = build_status_view(build_request, True)
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
repo_string = '%s/%s' % (namespace, repository)
|
||||||
|
@ -437,6 +456,36 @@ class TriggerBuildList(RepositoryParamResource):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||||
|
@internal_only
|
||||||
|
class BuildTriggerFieldValues(RepositoryParamResource):
|
||||||
|
""" Custom verb to fetch a values list for a particular field name. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('listTriggerFieldValues')
|
||||||
|
def get(self, namespace, repository, trigger_uuid, field_name):
|
||||||
|
""" List the field values for a custom run field. """
|
||||||
|
try:
|
||||||
|
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||||
|
except model.InvalidBuildTriggerException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
user_permission = UserAdminPermission(trigger.connected_user.username)
|
||||||
|
if user_permission.can():
|
||||||
|
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
|
||||||
|
values = trigger_handler.list_field_values(trigger.auth_token, json.loads(trigger.config),
|
||||||
|
field_name)
|
||||||
|
|
||||||
|
if values is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'values': values
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
|
|
|
@ -7,11 +7,13 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||||
|
|
||||||
from app import app, billing as stripe, authentication
|
from app import app, billing as stripe, authentication
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin, path_param,
|
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||||
InvalidToken, require_scope, format_date, hide_if, show_if, license_error,
|
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||||
define_json_response)
|
license_error, require_fresh_login, path_param, define_json_response)
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
|
from endpoints.api.team import try_accept_invite
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import get_plan
|
from data.billing import get_plan
|
||||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||||
|
@ -19,7 +21,8 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email)
|
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
|
||||||
|
from util.names import parse_single_urn
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -40,9 +43,15 @@ def user_view(user):
|
||||||
organizations = model.get_user_organizations(user.username)
|
organizations = model.get_user_organizations(user.username)
|
||||||
|
|
||||||
def login_view(login):
|
def login_view(login):
|
||||||
|
try:
|
||||||
|
metadata = json.loads(login.metadata_json)
|
||||||
|
except:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'service': login.service.name,
|
'service': login.service.name,
|
||||||
'service_identifier': login.service_ident,
|
'service_identifier': login.service_ident,
|
||||||
|
'metadata': metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
logins = model.list_federated_logins(user)
|
logins = model.list_federated_logins(user)
|
||||||
|
@ -89,6 +98,7 @@ class User(ApiResource):
|
||||||
""" Operations related to users. """
|
""" Operations related to users. """
|
||||||
schemas = {
|
schemas = {
|
||||||
'NewUser': {
|
'NewUser': {
|
||||||
|
|
||||||
'id': 'NewUser',
|
'id': 'NewUser',
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'Fields which must be specified for a new user.',
|
'description': 'Fields which must be specified for a new user.',
|
||||||
|
@ -185,6 +195,7 @@ class User(ApiResource):
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
|
@require_fresh_login
|
||||||
@nickname('changeUserDetails')
|
@nickname('changeUserDetails')
|
||||||
@internal_only
|
@internal_only
|
||||||
@validate_json_request('UpdateUser')
|
@validate_json_request('UpdateUser')
|
||||||
|
@ -194,12 +205,15 @@ class User(ApiResource):
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'password' in user_data:
|
if 'password' in user_data:
|
||||||
logger.debug('Changing password for user: %s', user.username)
|
logger.debug('Changing password for user: %s', user.username)
|
||||||
log_action('account_change_password', user.username)
|
log_action('account_change_password', user.username)
|
||||||
model.change_password(user, user_data['password'])
|
model.change_password(user, user_data['password'])
|
||||||
|
|
||||||
|
if features.MAILING:
|
||||||
|
send_password_changed(user.username, user.email)
|
||||||
|
|
||||||
if 'invoice_email' in user_data:
|
if 'invoice_email' in user_data:
|
||||||
logger.debug('Changing invoice_email for user: %s', user.username)
|
logger.debug('Changing invoice_email for user: %s', user.username)
|
||||||
model.change_invoice_email(user, user_data['invoice_email'])
|
model.change_invoice_email(user, user_data['invoice_email'])
|
||||||
|
@ -210,22 +224,30 @@ class User(ApiResource):
|
||||||
# Email already used.
|
# Email already used.
|
||||||
raise request_error(message='E-mail address already used')
|
raise request_error(message='E-mail address already used')
|
||||||
|
|
||||||
logger.debug('Sending email to change email address for user: %s',
|
if features.MAILING:
|
||||||
user.username)
|
logger.debug('Sending email to change email address for user: %s',
|
||||||
code = model.create_confirm_email_code(user, new_email=new_email)
|
user.username)
|
||||||
send_change_email(user.username, user_data['email'], code.code)
|
code = model.create_confirm_email_code(user, new_email=new_email)
|
||||||
|
send_change_email(user.username, user_data['email'], code.code)
|
||||||
|
else:
|
||||||
|
model.update_email(user, new_email, auto_verify=not features.MAILING)
|
||||||
|
|
||||||
except model.InvalidPasswordException, ex:
|
except model.InvalidPasswordException, ex:
|
||||||
raise request_error(exception=ex)
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
|
@show_if(features.USER_CREATION)
|
||||||
@nickname('createNewUser')
|
@nickname('createNewUser')
|
||||||
|
@parse_args
|
||||||
|
@query_param('inviteCode', 'Invitation code given for creating the user.', type=str,
|
||||||
|
default='')
|
||||||
@internal_only
|
@internal_only
|
||||||
@validate_json_request('NewUser')
|
@validate_json_request('NewUser')
|
||||||
def post(self):
|
def post(self, args):
|
||||||
""" Create a new user. """
|
""" Create a new user. """
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
invite_code = args['inviteCode']
|
||||||
|
|
||||||
existing_user = model.get_user(user_data['username'])
|
existing_user = model.get_user(user_data['username'])
|
||||||
if existing_user:
|
if existing_user:
|
||||||
|
@ -233,10 +255,29 @@ class User(ApiResource):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_user = model.create_user(user_data['username'], user_data['password'],
|
new_user = model.create_user(user_data['username'], user_data['password'],
|
||||||
user_data['email'])
|
user_data['email'], auto_verify=not features.MAILING)
|
||||||
code = model.create_confirm_email_code(new_user)
|
|
||||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
# Handle any invite codes.
|
||||||
return 'Created', 201
|
parsed_invite = parse_single_urn(invite_code)
|
||||||
|
if parsed_invite is not None:
|
||||||
|
if parsed_invite[0] == 'teaminvite':
|
||||||
|
# Add the user to the team.
|
||||||
|
try:
|
||||||
|
try_accept_invite(invite_code, new_user)
|
||||||
|
except model.DataModelException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if features.MAILING:
|
||||||
|
code = model.create_confirm_email_code(new_user)
|
||||||
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||||
|
return {
|
||||||
|
'awaiting_verification': True
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
common_login(new_user)
|
||||||
|
return user_view(new_user)
|
||||||
|
|
||||||
except model.TooManyUsersException as ex:
|
except model.TooManyUsersException as ex:
|
||||||
raise license_error(exception=ex)
|
raise license_error(exception=ex)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
|
@ -399,6 +440,37 @@ class Signin(ApiResource):
|
||||||
return conduct_signin(username, password)
|
return conduct_signin(username, password)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/signin/verify')
|
||||||
|
@internal_only
|
||||||
|
class VerifyUser(ApiResource):
|
||||||
|
""" Operations for verifying the existing user. """
|
||||||
|
schemas = {
|
||||||
|
'VerifyUser': {
|
||||||
|
'id': 'VerifyUser',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Information required to verify the signed in user.',
|
||||||
|
'required': [
|
||||||
|
'password',
|
||||||
|
],
|
||||||
|
'properties': {
|
||||||
|
'password': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The user\'s password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('verifyUser')
|
||||||
|
@validate_json_request('VerifyUser')
|
||||||
|
def post(self):
|
||||||
|
""" Verifies the signed in the user with the specified credentials. """
|
||||||
|
signin_data = request.get_json()
|
||||||
|
password = signin_data['password']
|
||||||
|
return conduct_signin(get_authenticated_user().username, password)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/signout')
|
@resource('/v1/signout')
|
||||||
@internal_only
|
@internal_only
|
||||||
class Signout(ApiResource):
|
class Signout(ApiResource):
|
||||||
|
@ -411,7 +483,21 @@ class Signout(ApiResource):
|
||||||
return {'success': True}
|
return {'success': True}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/detachexternal/<servicename>')
|
||||||
|
@internal_only
|
||||||
|
class DetachExternal(ApiResource):
|
||||||
|
""" Resource for detaching an external login. """
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('detachExternalLogin')
|
||||||
|
def post(self, servicename):
|
||||||
|
""" Request that the current user be detached from the external login service. """
|
||||||
|
model.detach_external_login(get_authenticated_user(), servicename)
|
||||||
|
return {'success': True}
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/recovery")
|
@resource("/v1/recovery")
|
||||||
|
@show_if(features.MAILING)
|
||||||
@internal_only
|
@internal_only
|
||||||
class Recovery(ApiResource):
|
class Recovery(ApiResource):
|
||||||
""" Resource for requesting a password recovery email. """
|
""" Resource for requesting a password recovery email. """
|
||||||
|
@ -446,11 +532,24 @@ class Recovery(ApiResource):
|
||||||
@internal_only
|
@internal_only
|
||||||
class UserNotificationList(ApiResource):
|
class UserNotificationList(ApiResource):
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
|
@parse_args
|
||||||
|
@query_param('page', 'Offset page number. (int)', type=int, default=0)
|
||||||
|
@query_param('limit', 'Limit on the number of results (int)', type=int, default=5)
|
||||||
@nickname('listUserNotifications')
|
@nickname('listUserNotifications')
|
||||||
def get(self):
|
def get(self, args):
|
||||||
notifications = model.list_notifications(get_authenticated_user())
|
page = args['page']
|
||||||
|
limit = args['limit']
|
||||||
|
|
||||||
|
notifications = list(model.list_notifications(get_authenticated_user(), page=page, limit=limit + 1))
|
||||||
|
has_more = False
|
||||||
|
|
||||||
|
if len(notifications) > limit:
|
||||||
|
has_more = True
|
||||||
|
notifications = notifications[0:limit]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'notifications': [notification_view(notification) for notification in notifications]
|
'notifications': [notification_view(notification) for notification in notifications],
|
||||||
|
'additional': has_more
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,14 @@ from flask import request, redirect, url_for, Blueprint
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from endpoints.common import render_page_template, common_login, route_show_if
|
from endpoints.common import render_page_template, common_login, route_show_if
|
||||||
from app import app, analytics
|
from app import app, analytics, get_app_url
|
||||||
from data import model
|
from data import model
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
from util.validation import generate_valid_usernames
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from auth.permissions import AdministerRepositoryPermission
|
from auth.permissions import AdministerRepositoryPermission
|
||||||
from auth.auth import require_session_login
|
from auth.auth import require_session_login
|
||||||
|
from peewee import IntegrityError
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -20,20 +22,40 @@ client = app.config['HTTPCLIENT']
|
||||||
|
|
||||||
callback = Blueprint('callback', __name__)
|
callback = Blueprint('callback', __name__)
|
||||||
|
|
||||||
|
def render_ologin_error(service_name,
|
||||||
|
error_message='Could not load user data. The token may have expired.'):
|
||||||
|
return render_page_template('ologinerror.html', service_name=service_name,
|
||||||
|
error_message=error_message,
|
||||||
|
service_url=get_app_url(),
|
||||||
|
user_creation=features.USER_CREATION)
|
||||||
|
|
||||||
def exchange_github_code_for_token(code, for_login=True):
|
def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False,
|
||||||
|
redirect_suffix=''):
|
||||||
code = request.args.get('code')
|
code = request.args.get('code')
|
||||||
|
id_config = service_name + '_LOGIN_CLIENT_ID' if for_login else service_name + '_CLIENT_ID'
|
||||||
|
secret_config = service_name + '_LOGIN_CLIENT_SECRET' if for_login else service_name + '_CLIENT_SECRET'
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'],
|
'client_id': app.config[id_config],
|
||||||
'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'],
|
'client_secret': app.config[secret_config],
|
||||||
'code': code,
|
'code': code,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'redirect_uri': '%s://%s/oauth2/%s/callback%s' % (app.config['PREFERRED_URL_SCHEME'],
|
||||||
|
app.config['SERVER_HOSTNAME'],
|
||||||
|
service_name.lower(),
|
||||||
|
redirect_suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
get_access_token = client.post(app.config['GITHUB_TOKEN_URL'],
|
if form_encode:
|
||||||
params=payload, headers=headers)
|
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
|
||||||
|
data=payload, headers=headers)
|
||||||
|
else:
|
||||||
|
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
|
||||||
|
params=payload, headers=headers)
|
||||||
|
|
||||||
json_data = get_access_token.json()
|
json_data = get_access_token.json()
|
||||||
if not json_data:
|
if not json_data:
|
||||||
|
@ -52,17 +74,87 @@ def get_github_user(token):
|
||||||
return get_user.json()
|
return get_user.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_google_user(token):
|
||||||
|
token_param = {
|
||||||
|
'access_token': token,
|
||||||
|
'alt': 'json',
|
||||||
|
}
|
||||||
|
|
||||||
|
get_user = client.get(app.config['GOOGLE_USER_URL'], params=token_param)
|
||||||
|
return get_user.json()
|
||||||
|
|
||||||
|
def conduct_oauth_login(service_name, user_id, username, email, metadata={}):
|
||||||
|
to_login = model.verify_federated_login(service_name.lower(), user_id)
|
||||||
|
if not to_login:
|
||||||
|
# See if we can create a new user.
|
||||||
|
if not features.USER_CREATION:
|
||||||
|
error_message = 'User creation is disabled. Please contact your administrator'
|
||||||
|
return render_ologin_error(service_name, error_message)
|
||||||
|
|
||||||
|
# Try to create the user
|
||||||
|
try:
|
||||||
|
valid = next(generate_valid_usernames(username))
|
||||||
|
to_login = model.create_federated_user(valid, email, service_name.lower(),
|
||||||
|
user_id, set_password_notification=True,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
|
# Success, tell analytics
|
||||||
|
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
|
||||||
|
|
||||||
|
state = request.args.get('state', None)
|
||||||
|
if state:
|
||||||
|
logger.debug('Aliasing with state: %s' % state)
|
||||||
|
analytics.alias(to_login.username, state)
|
||||||
|
|
||||||
|
except model.DataModelException, ex:
|
||||||
|
return render_ologin_error(service_name, ex.message)
|
||||||
|
|
||||||
|
if common_login(to_login):
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
return render_ologin_error(service_name)
|
||||||
|
|
||||||
|
def get_google_username(user_data):
|
||||||
|
username = user_data['email']
|
||||||
|
at = username.find('@')
|
||||||
|
if at > 0:
|
||||||
|
username = username[0:at]
|
||||||
|
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
@callback.route('/google/callback', methods=['GET'])
|
||||||
|
@route_show_if(features.GOOGLE_LOGIN)
|
||||||
|
def google_oauth_callback():
|
||||||
|
error = request.args.get('error', None)
|
||||||
|
if error:
|
||||||
|
return render_ologin_error('Google', error)
|
||||||
|
|
||||||
|
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE', form_encode=True)
|
||||||
|
user_data = get_google_user(token)
|
||||||
|
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
|
||||||
|
return render_ologin_error('Google')
|
||||||
|
|
||||||
|
username = get_google_username(user_data)
|
||||||
|
metadata = {
|
||||||
|
'service_username': user_data['email']
|
||||||
|
}
|
||||||
|
|
||||||
|
return conduct_oauth_login('Google', user_data['id'], username, user_data['email'],
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
|
|
||||||
@callback.route('/github/callback', methods=['GET'])
|
@callback.route('/github/callback', methods=['GET'])
|
||||||
@route_show_if(features.GITHUB_LOGIN)
|
@route_show_if(features.GITHUB_LOGIN)
|
||||||
def github_oauth_callback():
|
def github_oauth_callback():
|
||||||
error = request.args.get('error', None)
|
error = request.args.get('error', None)
|
||||||
if error:
|
if error:
|
||||||
return render_page_template('githuberror.html', error_message=error)
|
return render_ologin_error('GitHub', error)
|
||||||
|
|
||||||
token = exchange_github_code_for_token(request.args.get('code'))
|
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
|
||||||
user_data = get_github_user(token)
|
user_data = get_github_user(token)
|
||||||
if not user_data:
|
if not user_data or not 'login' in user_data:
|
||||||
return render_page_template('githuberror.html', error_message='Could not load user data')
|
return render_ologin_error('GitHub')
|
||||||
|
|
||||||
username = user_data['login']
|
username = user_data['login']
|
||||||
github_id = user_data['id']
|
github_id = user_data['id']
|
||||||
|
@ -84,42 +176,67 @@ def github_oauth_callback():
|
||||||
if user_email['primary']:
|
if user_email['primary']:
|
||||||
break
|
break
|
||||||
|
|
||||||
to_login = model.verify_federated_login('github', github_id)
|
metadata = {
|
||||||
if not to_login:
|
'service_username': username
|
||||||
# try to create the user
|
}
|
||||||
try:
|
|
||||||
to_login = model.create_federated_user(username, found_email, 'github',
|
|
||||||
github_id, set_password_notification=True)
|
|
||||||
|
|
||||||
# Success, tell analytics
|
return conduct_oauth_login('github', github_id, username, found_email, metadata=metadata)
|
||||||
analytics.track(to_login.username, 'register', {'service': 'github'})
|
|
||||||
|
|
||||||
state = request.args.get('state', None)
|
|
||||||
if state:
|
|
||||||
logger.debug('Aliasing with state: %s' % state)
|
|
||||||
analytics.alias(to_login.username, state)
|
|
||||||
|
|
||||||
except model.DataModelException, ex:
|
@callback.route('/google/callback/attach', methods=['GET'])
|
||||||
return render_page_template('githuberror.html', error_message=ex.message)
|
@route_show_if(features.GOOGLE_LOGIN)
|
||||||
|
@require_session_login
|
||||||
|
def google_oauth_attach():
|
||||||
|
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE',
|
||||||
|
redirect_suffix='/attach', form_encode=True)
|
||||||
|
|
||||||
if common_login(to_login):
|
user_data = get_google_user(token)
|
||||||
return redirect(url_for('web.index'))
|
if not user_data or not user_data.get('id', None):
|
||||||
|
return render_ologin_error('Google')
|
||||||
|
|
||||||
return render_page_template('githuberror.html')
|
google_id = user_data['id']
|
||||||
|
user_obj = current_user.db_user()
|
||||||
|
|
||||||
|
username = get_google_username(user_data)
|
||||||
|
metadata = {
|
||||||
|
'service_username': user_data['email']
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.attach_federated_login(user_obj, 'google', google_id, metadata=metadata)
|
||||||
|
except IntegrityError:
|
||||||
|
err = 'Google account %s is already attached to a %s account' % (
|
||||||
|
username, app.config['REGISTRY_TITLE_SHORT'])
|
||||||
|
return render_ologin_error('Google', err)
|
||||||
|
|
||||||
|
return redirect(url_for('web.user'))
|
||||||
|
|
||||||
|
|
||||||
@callback.route('/github/callback/attach', methods=['GET'])
|
@callback.route('/github/callback/attach', methods=['GET'])
|
||||||
@route_show_if(features.GITHUB_LOGIN)
|
@route_show_if(features.GITHUB_LOGIN)
|
||||||
@require_session_login
|
@require_session_login
|
||||||
def github_oauth_attach():
|
def github_oauth_attach():
|
||||||
token = exchange_github_code_for_token(request.args.get('code'))
|
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
|
||||||
user_data = get_github_user(token)
|
user_data = get_github_user(token)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
return render_page_template('githuberror.html', error_message='Could not load user data')
|
return render_ologin_error('GitHub')
|
||||||
|
|
||||||
github_id = user_data['id']
|
github_id = user_data['id']
|
||||||
user_obj = current_user.db_user()
|
user_obj = current_user.db_user()
|
||||||
model.attach_federated_login(user_obj, 'github', github_id)
|
|
||||||
|
username = user_data['login']
|
||||||
|
metadata = {
|
||||||
|
'service_username': username
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.attach_federated_login(user_obj, 'github', github_id, metadata=metadata)
|
||||||
|
except IntegrityError:
|
||||||
|
err = 'Github account %s is already attached to a %s account' % (
|
||||||
|
username, app.config['REGISTRY_TITLE_SHORT'])
|
||||||
|
|
||||||
|
return render_ologin_error('GitHub', err)
|
||||||
|
|
||||||
return redirect(url_for('web.user'))
|
return redirect(url_for('web.user'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,7 +247,8 @@ def github_oauth_attach():
|
||||||
def attach_github_build_trigger(namespace, repository):
|
def attach_github_build_trigger(namespace, repository):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
token = exchange_github_code_for_token(request.args.get('code'), for_login=False)
|
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB',
|
||||||
|
for_login=False)
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
if not repo:
|
if not repo:
|
||||||
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
||||||
|
|
|
@ -2,8 +2,9 @@ import logging
|
||||||
import urlparse
|
import urlparse
|
||||||
import json
|
import json
|
||||||
import string
|
import string
|
||||||
|
import datetime
|
||||||
|
|
||||||
from flask import make_response, render_template, request, abort
|
from flask import make_response, render_template, request, abort, session
|
||||||
from flask.ext.login import login_user, UserMixin
|
from flask.ext.login import login_user, UserMixin
|
||||||
from flask.ext.principal import identity_changed
|
from flask.ext.principal import identity_changed
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
@ -81,20 +82,23 @@ def param_required(param_name):
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(username):
|
def load_user(user_db_id):
|
||||||
logger.debug('User loader loading deferred user: %s' % username)
|
logger.debug('User loader loading deferred user id: %s' % user_db_id)
|
||||||
return _LoginWrappedDBUser(username)
|
try:
|
||||||
|
user_db_id_int = int(user_db_id)
|
||||||
|
return _LoginWrappedDBUser(user_db_id_int)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class _LoginWrappedDBUser(UserMixin):
|
class _LoginWrappedDBUser(UserMixin):
|
||||||
def __init__(self, db_username, db_user=None):
|
def __init__(self, user_db_id, db_user=None):
|
||||||
|
self._db_id = user_db_id
|
||||||
self._db_username = db_username
|
|
||||||
self._db_user = db_user
|
self._db_user = db_user
|
||||||
|
|
||||||
def db_user(self):
|
def db_user(self):
|
||||||
if not self._db_user:
|
if not self._db_user:
|
||||||
self._db_user = model.get_user(self._db_username)
|
self._db_user = model.get_user_by_id(self._db_id)
|
||||||
return self._db_user
|
return self._db_user
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
|
@ -104,14 +108,15 @@ class _LoginWrappedDBUser(UserMixin):
|
||||||
return self.db_user().verified
|
return self.db_user().verified
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return unicode(self._db_username)
|
return unicode(self._db_id)
|
||||||
|
|
||||||
|
|
||||||
def common_login(db_user):
|
def common_login(db_user):
|
||||||
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
if login_user(_LoginWrappedDBUser(db_user.id, db_user)):
|
||||||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username', {scopes.DIRECT_LOGIN})
|
new_identity = QuayDeferredPermissionUser(db_user.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
|
session['login_time'] = datetime.datetime.now()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.debug('User could not be logged in, inactive?.')
|
logger.debug('User could not be logged in, inactive?.')
|
||||||
|
@ -200,7 +205,7 @@ def check_repository_usage(user_or_org, plan_found):
|
||||||
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
trigger=None, pull_robot_name=None):
|
trigger=None, pull_robot_name=None):
|
||||||
host = urlparse.urlparse(request.url).netloc
|
host = urlparse.urlparse(request.url).netloc
|
||||||
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
|
repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name)
|
||||||
|
|
||||||
token = model.create_access_token(repository, 'write')
|
token = model.create_access_token(repository, 'write')
|
||||||
logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s',
|
logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s',
|
||||||
|
@ -216,9 +221,9 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
dockerfile_id, build_name,
|
dockerfile_id, build_name,
|
||||||
trigger, pull_robot_name=pull_robot_name)
|
trigger, pull_robot_name=pull_robot_name)
|
||||||
|
|
||||||
dockerfile_build_queue.put([repository.namespace, repository.name], json.dumps({
|
dockerfile_build_queue.put([repository.namespace_user.username, repository.name], json.dumps({
|
||||||
'build_uuid': build_request.uuid,
|
'build_uuid': build_request.uuid,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace_user.username,
|
||||||
'repository': repository.name,
|
'repository': repository.name,
|
||||||
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
|
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
|
||||||
}), retries_remaining=1)
|
}), retries_remaining=1)
|
||||||
|
@ -226,7 +231,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
# Add the build to the repo's log.
|
# Add the build to the repo's log.
|
||||||
metadata = {
|
metadata = {
|
||||||
'repo': repository.name,
|
'repo': repository.name,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace_user.username,
|
||||||
'fileid': dockerfile_id,
|
'fileid': dockerfile_id,
|
||||||
'manual': manual,
|
'manual': manual,
|
||||||
}
|
}
|
||||||
|
@ -236,9 +241,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
metadata['config'] = json.loads(trigger.config)
|
metadata['config'] = json.loads(trigger.config)
|
||||||
metadata['service'] = trigger.service.name
|
metadata['service'] = trigger.service.name
|
||||||
|
|
||||||
model.log_action('build_dockerfile', repository.namespace,
|
model.log_action('build_dockerfile', repository.namespace_user.username, ip=request.remote_addr,
|
||||||
ip=request.remote_addr, metadata=metadata,
|
metadata=metadata, repository=repository)
|
||||||
repository=repository)
|
|
||||||
|
|
||||||
# Add notifications for the build queue.
|
# Add notifications for the build queue.
|
||||||
profile.debug('Adding notifications for repository')
|
profile.debug('Adding notifications for repository')
|
||||||
|
|
|
@ -19,6 +19,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from endpoints.notificationhelper import spawn_notification
|
from endpoints.notificationhelper import spawn_notification
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
profile = logging.getLogger('application.profiler')
|
profile = logging.getLogger('application.profiler')
|
||||||
|
@ -65,7 +66,13 @@ def generate_headers(role='read'):
|
||||||
@index.route('/users', methods=['POST'])
|
@index.route('/users', methods=['POST'])
|
||||||
@index.route('/users/', methods=['POST'])
|
@index.route('/users/', methods=['POST'])
|
||||||
def create_user():
|
def create_user():
|
||||||
|
if not features.USER_CREATION:
|
||||||
|
abort(400, 'User creation is disabled. Please speak to your administrator.')
|
||||||
|
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
if not 'username' in user_data:
|
||||||
|
abort(400, 'Missing username')
|
||||||
|
|
||||||
username = user_data['username']
|
username = user_data['username']
|
||||||
password = user_data.get('password', '')
|
password = user_data.get('password', '')
|
||||||
|
|
||||||
|
@ -413,8 +420,39 @@ def put_repository_auth(namespace, repository):
|
||||||
|
|
||||||
|
|
||||||
@index.route('/search', methods=['GET'])
|
@index.route('/search', methods=['GET'])
|
||||||
|
@process_auth
|
||||||
def get_search():
|
def get_search():
|
||||||
abort(501, 'Not Implemented', issue='not-implemented')
|
def result_view(repo):
|
||||||
|
return {
|
||||||
|
"name": repo.namespace_user.username + '/' + repo.name,
|
||||||
|
"description": repo.description
|
||||||
|
}
|
||||||
|
|
||||||
|
query = request.args.get('q')
|
||||||
|
|
||||||
|
username = None
|
||||||
|
user = get_authenticated_user()
|
||||||
|
if user is not None:
|
||||||
|
username = user.username
|
||||||
|
|
||||||
|
if query:
|
||||||
|
matching = model.get_matching_repositories(query, username)
|
||||||
|
else:
|
||||||
|
matching = []
|
||||||
|
|
||||||
|
results = [result_view(repo) for repo in matching
|
||||||
|
if (repo.visibility.name == 'public' or
|
||||||
|
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"query": query,
|
||||||
|
"num_results": len(results),
|
||||||
|
"results" : results
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = make_response(json.dumps(data), 200)
|
||||||
|
resp.mimetype = 'application/json'
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@index.route('/_ping')
|
@index.route('/_ping')
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
import io
|
|
||||||
import os.path
|
|
||||||
import tarfile
|
|
||||||
import base64
|
|
||||||
|
|
||||||
from notificationhelper import build_event_data
|
from notificationhelper import build_event_data
|
||||||
|
|
||||||
|
@ -15,6 +11,13 @@ class NotificationEvent(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
"""
|
||||||
|
Returns a 'level' representing the severity of the event.
|
||||||
|
Valid values are: 'info', 'warning', 'error', 'primary'
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_summary(self, event_data, notification_data):
|
def get_summary(self, event_data, notification_data):
|
||||||
"""
|
"""
|
||||||
Returns a human readable one-line summary for the given notification data.
|
Returns a human readable one-line summary for the given notification data.
|
||||||
|
@ -55,6 +58,9 @@ class RepoPushEvent(NotificationEvent):
|
||||||
def event_name(cls):
|
def event_name(cls):
|
||||||
return 'repo_push'
|
return 'repo_push'
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
return 'info'
|
||||||
|
|
||||||
def get_summary(self, event_data, notification_data):
|
def get_summary(self, event_data, notification_data):
|
||||||
return 'Repository %s updated' % (event_data['repository'])
|
return 'Repository %s updated' % (event_data['repository'])
|
||||||
|
|
||||||
|
@ -87,6 +93,9 @@ class BuildQueueEvent(NotificationEvent):
|
||||||
@classmethod
|
@classmethod
|
||||||
def event_name(cls):
|
def event_name(cls):
|
||||||
return 'build_queued'
|
return 'build_queued'
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
return 'info'
|
||||||
|
|
||||||
def get_sample_data(self, repository):
|
def get_sample_data(self, repository):
|
||||||
build_uuid = 'fake-build-id'
|
build_uuid = 'fake-build-id'
|
||||||
|
@ -127,6 +136,9 @@ class BuildStartEvent(NotificationEvent):
|
||||||
def event_name(cls):
|
def event_name(cls):
|
||||||
return 'build_start'
|
return 'build_start'
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
return 'info'
|
||||||
|
|
||||||
def get_sample_data(self, repository):
|
def get_sample_data(self, repository):
|
||||||
build_uuid = 'fake-build-id'
|
build_uuid = 'fake-build-id'
|
||||||
|
|
||||||
|
@ -155,6 +167,9 @@ class BuildSuccessEvent(NotificationEvent):
|
||||||
def event_name(cls):
|
def event_name(cls):
|
||||||
return 'build_success'
|
return 'build_success'
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
return 'primary'
|
||||||
|
|
||||||
def get_sample_data(self, repository):
|
def get_sample_data(self, repository):
|
||||||
build_uuid = 'fake-build-id'
|
build_uuid = 'fake-build-id'
|
||||||
|
|
||||||
|
@ -183,7 +198,12 @@ class BuildFailureEvent(NotificationEvent):
|
||||||
def event_name(cls):
|
def event_name(cls):
|
||||||
return 'build_failure'
|
return 'build_failure'
|
||||||
|
|
||||||
|
def get_level(self, event_data, notification_data):
|
||||||
|
return 'error'
|
||||||
|
|
||||||
def get_sample_data(self, repository):
|
def get_sample_data(self, repository):
|
||||||
|
build_uuid = 'fake-build-id'
|
||||||
|
|
||||||
return build_event_data(repository, {
|
return build_event_data(repository, {
|
||||||
'build_id': build_uuid,
|
'build_id': build_uuid,
|
||||||
'build_name': 'some-fake-build',
|
'build_name': 'some-fake-build',
|
||||||
|
|
|
@ -4,7 +4,7 @@ from data import model
|
||||||
import json
|
import json
|
||||||
|
|
||||||
def build_event_data(repo, extra_data={}, subpage=None):
|
def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
repo_string = '%s/%s' % (repo.namespace, repo.name)
|
repo_string = '%s/%s' % (repo.namespace_user.username, repo.name)
|
||||||
homepage = '%s://%s/repository/%s' % (app.config['PREFERRED_URL_SCHEME'],
|
homepage = '%s://%s/repository/%s' % (app.config['PREFERRED_URL_SCHEME'],
|
||||||
app.config['SERVER_HOSTNAME'],
|
app.config['SERVER_HOSTNAME'],
|
||||||
repo_string)
|
repo_string)
|
||||||
|
@ -17,7 +17,7 @@ def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
|
|
||||||
event_data = {
|
event_data = {
|
||||||
'repository': repo_string,
|
'repository': repo_string,
|
||||||
'namespace': repo.namespace,
|
'namespace': repo.namespace_user.username,
|
||||||
'name': repo.name,
|
'name': repo.name,
|
||||||
'docker_url': '%s/%s' % (app.config['SERVER_HOSTNAME'], repo_string),
|
'docker_url': '%s/%s' % (app.config['SERVER_HOSTNAME'], repo_string),
|
||||||
'homepage': homepage,
|
'homepage': homepage,
|
||||||
|
@ -30,7 +30,7 @@ def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
def build_notification_data(notification, event_data):
|
def build_notification_data(notification, event_data):
|
||||||
return {
|
return {
|
||||||
'notification_uuid': notification.uuid,
|
'notification_uuid': notification.uuid,
|
||||||
'repository_namespace': notification.repository.namespace,
|
'repository_namespace': notification.repository.namespace_user.username,
|
||||||
'repository_name': notification.repository.name,
|
'repository_name': notification.repository.name,
|
||||||
'event_data': event_data
|
'event_data': event_data
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,9 @@ def build_notification_data(notification, event_data):
|
||||||
def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
|
def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
|
||||||
event_data = build_event_data(repo, extra_data=extra_data, subpage=subpage)
|
event_data = build_event_data(repo, extra_data=extra_data, subpage=subpage)
|
||||||
|
|
||||||
notifications = model.list_repo_notifications(repo.namespace, repo.name, event_name=event_name)
|
notifications = model.list_repo_notifications(repo.namespace_user.username, repo.name,
|
||||||
|
event_name=event_name)
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
notification_data = build_notification_data(notification, event_data)
|
notification_data = build_notification_data(notification, event_data)
|
||||||
path = [repo.namespace, repo.name, event_name] + pathargs
|
path = [repo.namespace_user.username, repo.name, event_name] + pathargs
|
||||||
notification_queue.put(path, json.dumps(notification_data))
|
notification_queue.put(path, json.dumps(notification_data))
|
||||||
|
|
|
@ -4,10 +4,13 @@ import os.path
|
||||||
import tarfile
|
import tarfile
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
|
||||||
from flask.ext.mail import Message
|
from flask.ext.mail import Message
|
||||||
from app import mail, app
|
from app import mail, app, get_app_url
|
||||||
from data import model
|
from data import model
|
||||||
|
from workers.worker import JobException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -17,6 +20,9 @@ class InvalidNotificationMethodException(Exception):
|
||||||
class CannotValidateNotificationMethodException(Exception):
|
class CannotValidateNotificationMethodException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class NotificationMethodPerformException(JobException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotificationMethod(object):
|
class NotificationMethod(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -82,7 +88,7 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
return (True, 'Unknown organization %s' % target_info['name'], None)
|
return (True, 'Unknown organization %s' % target_info['name'], None)
|
||||||
|
|
||||||
# Only repositories under the organization can cause notifications to that org.
|
# Only repositories under the organization can cause notifications to that org.
|
||||||
if target_info['name'] != repository.namespace:
|
if target_info['name'] != repository.namespace_user.username:
|
||||||
return (False, 'Organization name must match repository namespace')
|
return (False, 'Organization name must match repository namespace')
|
||||||
|
|
||||||
return (True, None, [target])
|
return (True, None, [target])
|
||||||
|
@ -90,7 +96,7 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
# Lookup the team.
|
# Lookup the team.
|
||||||
team = None
|
team = None
|
||||||
try:
|
try:
|
||||||
team = model.get_organization_team(repository.namespace, target_info['name'])
|
team = model.get_organization_team(repository.namespace_user.username, target_info['name'])
|
||||||
except model.InvalidTeamException:
|
except model.InvalidTeamException:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return (True, 'Unknown team %s' % target_info['name'], None)
|
return (True, 'Unknown team %s' % target_info['name'], None)
|
||||||
|
@ -103,19 +109,18 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
repository = notification.repository
|
repository = notification.repository
|
||||||
if not repository:
|
if not repository:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return True
|
return
|
||||||
|
|
||||||
# Lookup the target user or team to which we'll send the notification.
|
# Lookup the target user or team to which we'll send the notification.
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
status, err_message, target_users = self.find_targets(repository, config_data)
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||||
if not status:
|
if not status:
|
||||||
return False
|
raise NotificationMethodPerformException(err_message)
|
||||||
|
|
||||||
# For each of the target users, create a notification.
|
# For each of the target users, create a notification.
|
||||||
for target_user in set(target_users or []):
|
for target_user in set(target_users or []):
|
||||||
model.create_notification(event_handler.event_name(), target_user,
|
model.create_notification(event_handler.event_name(), target_user,
|
||||||
metadata=notification_data['event_data'])
|
metadata=notification_data['event_data'])
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class EmailMethod(NotificationMethod):
|
class EmailMethod(NotificationMethod):
|
||||||
|
@ -128,7 +133,8 @@ class EmailMethod(NotificationMethod):
|
||||||
if not email:
|
if not email:
|
||||||
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
||||||
|
|
||||||
record = model.get_email_authorized_for_repo(repository.namespace, repository.name, email)
|
record = model.get_email_authorized_for_repo(repository.namespace_user.username,
|
||||||
|
repository.name, email)
|
||||||
if not record or not record.confirmed:
|
if not record or not record.confirmed:
|
||||||
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
||||||
'is not authorized to receive '
|
'is not authorized to receive '
|
||||||
|
@ -139,7 +145,7 @@ class EmailMethod(NotificationMethod):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
email = config_data.get('email', '')
|
email = config_data.get('email', '')
|
||||||
if not email:
|
if not email:
|
||||||
return False
|
return
|
||||||
|
|
||||||
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
||||||
sender='support@quay.io',
|
sender='support@quay.io',
|
||||||
|
@ -151,9 +157,7 @@ class EmailMethod(NotificationMethod):
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception('Email was unable to be sent: %s' % ex.message)
|
logger.exception('Email was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookMethod(NotificationMethod):
|
class WebhookMethod(NotificationMethod):
|
||||||
|
@ -170,7 +174,7 @@ class WebhookMethod(NotificationMethod):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
url = config_data.get('url', '')
|
url = config_data.get('url', '')
|
||||||
if not url:
|
if not url:
|
||||||
return False
|
return
|
||||||
|
|
||||||
payload = notification_data['event_data']
|
payload = notification_data['event_data']
|
||||||
headers = {'Content-type': 'application/json'}
|
headers = {'Content-type': 'application/json'}
|
||||||
|
@ -178,12 +182,197 @@ class WebhookMethod(NotificationMethod):
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
if resp.status_code/100 != 2:
|
if resp.status_code/100 != 2:
|
||||||
logger.error('%s response for webhook to url: %s' % (resp.status_code,
|
error_message = '%s response for webhook to url: %s' % (resp.status_code, url)
|
||||||
url))
|
logger.error(error_message)
|
||||||
return False
|
logger.error(resp.content)
|
||||||
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
class FlowdockMethod(NotificationMethod):
|
||||||
|
""" Method for sending notifications to Flowdock via the Team Inbox API:
|
||||||
|
https://www.flowdock.com/api/team-inbox
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'flowdock'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
token = config_data.get('flow_api_token', '')
|
||||||
|
if not token:
|
||||||
|
raise CannotValidateNotificationMethodException('Missing Flowdock API Token')
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
token = config_data.get('flow_api_token', '')
|
||||||
|
if not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
|
if not owner:
|
||||||
|
# Something went wrong.
|
||||||
|
return
|
||||||
|
|
||||||
|
url = 'https://api.flowdock.com/v1/messages/team_inbox/%s' % token
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
payload = {
|
||||||
|
'source': 'Quay',
|
||||||
|
'from_address': 'support@quay.io',
|
||||||
|
'subject': event_handler.get_summary(notification_data['event_data'], notification_data),
|
||||||
|
'content': event_handler.get_message(notification_data['event_data'], notification_data),
|
||||||
|
'from_name': owner.username,
|
||||||
|
'project': (notification.repository.namespace_user.username + ' ' +
|
||||||
|
notification.repository.name),
|
||||||
|
'tags': ['#' + event_handler.event_name()],
|
||||||
|
'link': notification_data['event_data']['homepage']
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
|
if resp.status_code/100 != 2:
|
||||||
|
error_message = '%s response for flowdock to url: %s' % (resp.status_code, url)
|
||||||
|
logger.error(error_message)
|
||||||
|
logger.error(resp.content)
|
||||||
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as ex:
|
||||||
|
logger.exception('Flowdock method was unable to be sent: %s' % ex.message)
|
||||||
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
|
|
||||||
|
class HipchatMethod(NotificationMethod):
|
||||||
|
""" Method for sending notifications to Hipchat via the API:
|
||||||
|
https://www.hipchat.com/docs/apiv2/method/send_room_notification
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'hipchat'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
if not config_data.get('notification_token', ''):
|
||||||
|
raise CannotValidateNotificationMethodException('Missing Hipchat Room Notification Token')
|
||||||
|
|
||||||
|
if not config_data.get('room_id', ''):
|
||||||
|
raise CannotValidateNotificationMethodException('Missing Hipchat Room ID')
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
|
||||||
|
token = config_data.get('notification_token', '')
|
||||||
|
room_id = config_data.get('room_id', '')
|
||||||
|
|
||||||
|
if not token or not room_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
|
if not owner:
|
||||||
|
# Something went wrong.
|
||||||
|
return
|
||||||
|
|
||||||
|
url = 'https://api.hipchat.com/v2/room/%s/notification?auth_token=%s' % (room_id, token)
|
||||||
|
|
||||||
|
level = event_handler.get_level(notification_data['event_data'], notification_data)
|
||||||
|
color = {
|
||||||
|
'info': 'gray',
|
||||||
|
'warning': 'yellow',
|
||||||
|
'error': 'red',
|
||||||
|
'primary': 'purple'
|
||||||
|
}.get(level, 'gray')
|
||||||
|
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
payload = {
|
||||||
|
'color': color,
|
||||||
|
'message': event_handler.get_message(notification_data['event_data'], notification_data),
|
||||||
|
'notify': level == 'error',
|
||||||
|
'message_format': 'html',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
|
if resp.status_code/100 != 2:
|
||||||
|
error_message = '%s response for hipchat to url: %s' % (resp.status_code, url)
|
||||||
|
logger.error(error_message)
|
||||||
|
logger.error(resp.content)
|
||||||
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as ex:
|
||||||
|
logger.exception('Hipchat method was unable to be sent: %s' % ex.message)
|
||||||
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMethod(NotificationMethod):
|
||||||
|
""" Method for sending notifications to Slack via the API:
|
||||||
|
https://api.slack.com/docs/attachments
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'slack'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
if not config_data.get('token', ''):
|
||||||
|
raise CannotValidateNotificationMethodException('Missing Slack Token')
|
||||||
|
|
||||||
|
if not config_data.get('subdomain', '').isalnum():
|
||||||
|
raise CannotValidateNotificationMethodException('Missing Slack Subdomain Name')
|
||||||
|
|
||||||
|
def formatForSlack(self, message):
|
||||||
|
message = message.replace('\n', '')
|
||||||
|
message = re.sub(r'\s+', ' ', message)
|
||||||
|
message = message.replace('<br>', '\n')
|
||||||
|
message = re.sub(r'<a href="(.+)">(.+)</a>', '<\\1|\\2>', message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
|
||||||
|
token = config_data.get('token', '')
|
||||||
|
subdomain = config_data.get('subdomain', '')
|
||||||
|
|
||||||
|
if not token or not subdomain:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
|
if not owner:
|
||||||
|
# Something went wrong.
|
||||||
|
return
|
||||||
|
|
||||||
|
url = 'https://%s.slack.com/services/hooks/incoming-webhook?token=%s' % (subdomain, token)
|
||||||
|
|
||||||
|
level = event_handler.get_level(notification_data['event_data'], notification_data)
|
||||||
|
color = {
|
||||||
|
'info': '#ffffff',
|
||||||
|
'warning': 'warning',
|
||||||
|
'error': 'danger',
|
||||||
|
'primary': 'good'
|
||||||
|
}.get(level, '#ffffff')
|
||||||
|
|
||||||
|
summary = event_handler.get_summary(notification_data['event_data'], notification_data)
|
||||||
|
message = event_handler.get_message(notification_data['event_data'], notification_data)
|
||||||
|
|
||||||
|
headers = {'Content-type': 'application/json'}
|
||||||
|
payload = {
|
||||||
|
'text': summary,
|
||||||
|
'username': 'quayiobot',
|
||||||
|
'attachments': [
|
||||||
|
{
|
||||||
|
'fallback': summary,
|
||||||
|
'text': self.formatForSlack(message),
|
||||||
|
'color': color
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
|
if resp.status_code/100 != 2:
|
||||||
|
error_message = '%s response for Slack to url: %s' % (resp.status_code, url)
|
||||||
|
logger.error(error_message)
|
||||||
|
logger.error(resp.content)
|
||||||
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as ex:
|
||||||
|
logger.exception('Slack method was unable to be sent: %s' % ex.message)
|
||||||
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from util.http import abort, exact_abort
|
||||||
from auth.permissions import (ReadRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission)
|
ModifyRepositoryPermission)
|
||||||
from data import model
|
from data import model
|
||||||
|
from util import gzipstream
|
||||||
|
|
||||||
|
|
||||||
registry = Blueprint('registry', __name__)
|
registry = Blueprint('registry', __name__)
|
||||||
|
@ -110,10 +111,10 @@ def head_image_layer(namespace, repository, image_id, headers):
|
||||||
|
|
||||||
extra_headers = {}
|
extra_headers = {}
|
||||||
|
|
||||||
# Add the Accept-Ranges header if the storage engine supports resumeable
|
# Add the Accept-Ranges header if the storage engine supports resumable
|
||||||
# downloads.
|
# downloads.
|
||||||
if store.get_supports_resumeable_downloads(repo_image.storage.locations):
|
if store.get_supports_resumable_downloads(repo_image.storage.locations):
|
||||||
profile.debug('Storage supports resumeable downloads')
|
profile.debug('Storage supports resumable downloads')
|
||||||
extra_headers['Accept-Ranges'] = 'bytes'
|
extra_headers['Accept-Ranges'] = 'bytes'
|
||||||
|
|
||||||
resp = make_response('')
|
resp = make_response('')
|
||||||
|
@ -193,21 +194,33 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
# encoding (Gunicorn)
|
# encoding (Gunicorn)
|
||||||
input_stream = request.environ['wsgi.input']
|
input_stream = request.environ['wsgi.input']
|
||||||
|
|
||||||
# compute checksums
|
# Create a socket reader to read the input stream containing the layer data.
|
||||||
csums = []
|
|
||||||
sr = SocketReader(input_stream)
|
sr = SocketReader(input_stream)
|
||||||
|
|
||||||
|
# Add a handler that store the data in storage.
|
||||||
tmp, store_hndlr = store.temp_store_handler()
|
tmp, store_hndlr = store.temp_store_handler()
|
||||||
sr.add_handler(store_hndlr)
|
sr.add_handler(store_hndlr)
|
||||||
|
|
||||||
|
# Add a handler to compute the uncompressed size of the layer.
|
||||||
|
uncompressed_size_info, size_hndlr = gzipstream.calculate_size_handler()
|
||||||
|
sr.add_handler(size_hndlr)
|
||||||
|
|
||||||
|
# Add a handler which computes the checksum.
|
||||||
h, sum_hndlr = checksums.simple_checksum_handler(json_data)
|
h, sum_hndlr = checksums.simple_checksum_handler(json_data)
|
||||||
sr.add_handler(sum_hndlr)
|
sr.add_handler(sum_hndlr)
|
||||||
|
|
||||||
|
# Stream write the data to storage.
|
||||||
store.stream_write(repo_image.storage.locations, layer_path, sr)
|
store.stream_write(repo_image.storage.locations, layer_path, sr)
|
||||||
|
|
||||||
|
# Append the computed checksum.
|
||||||
|
csums = []
|
||||||
csums.append('sha256:{0}'.format(h.hexdigest()))
|
csums.append('sha256:{0}'.format(h.hexdigest()))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image_size = tmp.tell()
|
image_size = tmp.tell()
|
||||||
|
|
||||||
# Save the size of the image.
|
# Save the size of the image.
|
||||||
model.set_image_size(image_id, namespace, repository, image_size)
|
model.set_image_size(image_id, namespace, repository, image_size, uncompressed_size_info.size)
|
||||||
|
|
||||||
tmp.seek(0)
|
tmp.seek(0)
|
||||||
csums.append(checksums.compute_tarsum(tmp, json_data))
|
csums.append(checksums.compute_tarsum(tmp, json_data))
|
||||||
|
@ -451,11 +464,6 @@ def put_image_json(namespace, repository, image_id):
|
||||||
|
|
||||||
set_uploading_flag(repo_image, True)
|
set_uploading_flag(repo_image, True)
|
||||||
|
|
||||||
# We cleanup any old checksum in case it's a retry after a fail
|
|
||||||
profile.debug('Cleanup old checksum')
|
|
||||||
repo_image.storage.checksum = None
|
|
||||||
repo_image.storage.save()
|
|
||||||
|
|
||||||
# If we reach that point, it means that this is a new image or a retry
|
# If we reach that point, it means that this is a new image or a retry
|
||||||
# on a failed push
|
# on a failed push
|
||||||
# save the metadata
|
# save the metadata
|
||||||
|
|
|
@ -36,6 +36,9 @@ class TriggerActivationException(Exception):
|
||||||
class TriggerDeactivationException(Exception):
|
class TriggerDeactivationException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class TriggerStartException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class ValidationRequestException(Exception):
|
class ValidationRequestException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -109,12 +112,19 @@ class BuildTrigger(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config, run_parameters = None):
|
||||||
"""
|
"""
|
||||||
Manually creates a repository build for this trigger.
|
Manually creates a repository build for this trigger.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def list_field_values(self, auth_token, config, field_name):
|
||||||
|
"""
|
||||||
|
Lists all values for the given custom trigger field. For example, a trigger might have a
|
||||||
|
field named "branches", and this method would return all branches.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def service_name(cls):
|
def service_name(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -291,6 +301,9 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
with tarfile.open(fileobj=tarball) as archive:
|
with tarfile.open(fileobj=tarball) as archive:
|
||||||
tarball_subdir = archive.getnames()[0]
|
tarball_subdir = archive.getnames()[0]
|
||||||
|
|
||||||
|
# Seek to position 0 to make boto multipart happy
|
||||||
|
tarball.seek(0)
|
||||||
|
|
||||||
dockerfile_id = user_files.store_file(tarball, TARBALL_MIME)
|
dockerfile_id = user_files.store_file(tarball, TARBALL_MIME)
|
||||||
|
|
||||||
logger.debug('Successfully prepared job')
|
logger.debug('Successfully prepared job')
|
||||||
|
@ -342,14 +355,37 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
|
return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
|
||||||
short_sha, ref)
|
short_sha, ref)
|
||||||
|
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config, run_parameters = None):
|
||||||
source = config['build_source']
|
try:
|
||||||
|
source = config['build_source']
|
||||||
|
run_parameters = run_parameters or {}
|
||||||
|
|
||||||
gh_client = self._get_client(auth_token)
|
gh_client = self._get_client(auth_token)
|
||||||
repo = gh_client.get_repo(source)
|
repo = gh_client.get_repo(source)
|
||||||
master = repo.get_branch(repo.default_branch)
|
master = repo.get_branch(repo.default_branch)
|
||||||
master_sha = master.commit.sha
|
master_sha = master.commit.sha
|
||||||
short_sha = GithubBuildTrigger.get_display_name(master_sha)
|
short_sha = GithubBuildTrigger.get_display_name(master_sha)
|
||||||
ref = 'refs/heads/%s' % repo.default_branch
|
ref = 'refs/heads/%s' % (run_parameters.get('branch_name') or repo.default_branch)
|
||||||
|
|
||||||
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
||||||
|
except GithubException as ghe:
|
||||||
|
raise TriggerStartException(ghe.data['message'])
|
||||||
|
|
||||||
|
|
||||||
|
def list_field_values(self, auth_token, config, field_name):
|
||||||
|
if field_name == 'branch_name':
|
||||||
|
gh_client = self._get_client(auth_token)
|
||||||
|
source = config['build_source']
|
||||||
|
repo = gh_client.get_repo(source)
|
||||||
|
branches = [branch['name'] for branch in repo.get_branches()]
|
||||||
|
|
||||||
|
if not repo.default_branch in branches:
|
||||||
|
branches.insert(0, repo.default_branch)
|
||||||
|
|
||||||
|
if branches[0] != repo.default_branch:
|
||||||
|
branches.remove(repo.default_branch)
|
||||||
|
branches.insert(0, repo.default_branch)
|
||||||
|
|
||||||
|
return branches
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -18,6 +18,7 @@ from endpoints.common import common_login, render_page_template, route_show_if,
|
||||||
from endpoints.csrf import csrf_protect, generate_csrf_token
|
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
|
from util.useremails import send_email_changed
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
@ -32,8 +33,8 @@ STATUS_TAGS = app.config['STATUS_TAGS']
|
||||||
@web.route('/', methods=['GET'], defaults={'path': ''})
|
@web.route('/', methods=['GET'], defaults={'path': ''})
|
||||||
@web.route('/organization/<path:path>', methods=['GET'])
|
@web.route('/organization/<path:path>', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
def index(path):
|
def index(path, **kwargs):
|
||||||
return render_page_template('index.html')
|
return render_page_template('index.html', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/500', methods=['GET'])
|
@web.route('/500', methods=['GET'])
|
||||||
|
@ -101,7 +102,7 @@ def superuser():
|
||||||
|
|
||||||
@web.route('/signin/')
|
@web.route('/signin/')
|
||||||
@no_cache
|
@no_cache
|
||||||
def signin():
|
def signin(redirect=None):
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,6 +124,13 @@ def new():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/confirminvite')
|
||||||
|
@no_cache
|
||||||
|
def confirm_invite():
|
||||||
|
code = request.values['code']
|
||||||
|
return index('', code=code)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/repository/', defaults={'path': ''})
|
@web.route('/repository/', defaults={'path': ''})
|
||||||
@web.route('/repository/<path:path>', methods=['GET'])
|
@web.route('/repository/<path:path>', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@ -215,6 +223,7 @@ def receipt():
|
||||||
|
|
||||||
|
|
||||||
@web.route('/authrepoemail', methods=['GET'])
|
@web.route('/authrepoemail', methods=['GET'])
|
||||||
|
@route_show_if(features.MAILING)
|
||||||
def confirm_repo_email():
|
def confirm_repo_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
record = None
|
record = None
|
||||||
|
@ -228,23 +237,27 @@ def confirm_repo_email():
|
||||||
Your E-mail address has been authorized to receive notifications for repository
|
Your E-mail address has been authorized to receive notifications for repository
|
||||||
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
||||||
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
||||||
record.repository.namespace, record.repository.name,
|
record.repository.namespace_user.username, record.repository.name,
|
||||||
record.repository.namespace, record.repository.name)
|
record.repository.namespace_user.username, record.repository.name)
|
||||||
|
|
||||||
return render_page_template('message.html', message=message)
|
return render_page_template('message.html', message=message)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/confirm', methods=['GET'])
|
@web.route('/confirm', methods=['GET'])
|
||||||
|
@route_show_if(features.MAILING)
|
||||||
def confirm_email():
|
def confirm_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
user = None
|
user = None
|
||||||
new_email = None
|
new_email = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user, new_email = model.confirm_user_email(code)
|
user, new_email, old_email = model.confirm_user_email(code)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
return render_page_template('confirmerror.html', error_message=ex.message)
|
return render_page_template('confirmerror.html', error_message=ex.message)
|
||||||
|
|
||||||
|
if new_email:
|
||||||
|
send_email_changed(user.username, old_email, new_email)
|
||||||
|
|
||||||
common_login(user)
|
common_login(user)
|
||||||
|
|
||||||
return redirect(url_for('web.user', tab='email')
|
return redirect(url_for('web.user', tab='email')
|
||||||
|
|
65
initdb.py
65
initdb.py
|
@ -51,7 +51,7 @@ def __gen_checksum(image_id):
|
||||||
|
|
||||||
|
|
||||||
def __gen_image_id(repo, image_num):
|
def __gen_image_id(repo, image_num):
|
||||||
str_to_hash = "%s/%s/%s" % (repo.namespace, repo.name, image_num)
|
str_to_hash = "%s/%s/%s" % (repo.namespace_user.username, repo.name, image_num)
|
||||||
|
|
||||||
h = hashlib.md5(str_to_hash)
|
h = hashlib.md5(str_to_hash)
|
||||||
return h.hexdigest() + h.hexdigest()
|
return h.hexdigest() + h.hexdigest()
|
||||||
|
@ -79,12 +79,12 @@ def __create_subtree(repo, structure, creator_username, parent):
|
||||||
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
||||||
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
|
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
|
||||||
command = json.dumps(command_list) if command_list else None
|
command = json.dumps(command_list) if command_list else None
|
||||||
new_image = model.set_image_metadata(docker_image_id, repo.namespace,
|
new_image = model.set_image_metadata(docker_image_id, repo.namespace_user.username, repo.name,
|
||||||
repo.name, str(creation_time),
|
str(creation_time), 'no comment', command, parent)
|
||||||
'no comment', command, parent)
|
|
||||||
|
|
||||||
model.set_image_size(docker_image_id, repo.namespace, repo.name,
|
compressed_size = random.randrange(1, 1024 * 1024 * 1024)
|
||||||
random.randrange(1, 1024 * 1024 * 1024))
|
model.set_image_size(docker_image_id, repo.namespace_user.username, repo.name, compressed_size,
|
||||||
|
int(compressed_size * 1.4))
|
||||||
|
|
||||||
# Populate the diff file
|
# Populate the diff file
|
||||||
diff_path = store.image_file_diffs_path(new_image.storage.uuid)
|
diff_path = store.image_file_diffs_path(new_image.storage.uuid)
|
||||||
|
@ -100,7 +100,7 @@ def __create_subtree(repo, structure, creator_username, parent):
|
||||||
last_node_tags = [last_node_tags]
|
last_node_tags = [last_node_tags]
|
||||||
|
|
||||||
for tag_name in last_node_tags:
|
for tag_name in last_node_tags:
|
||||||
model.create_or_update_tag(repo.namespace, repo.name, tag_name,
|
model.create_or_update_tag(repo.namespace_user.username, repo.name, tag_name,
|
||||||
new_image.docker_image_id)
|
new_image.docker_image_id)
|
||||||
|
|
||||||
for subtree in subtrees:
|
for subtree in subtrees:
|
||||||
|
@ -179,6 +179,8 @@ def initialize_database():
|
||||||
TeamRole.create(name='member')
|
TeamRole.create(name='member')
|
||||||
Visibility.create(name='public')
|
Visibility.create(name='public')
|
||||||
Visibility.create(name='private')
|
Visibility.create(name='private')
|
||||||
|
|
||||||
|
LoginService.create(name='google')
|
||||||
LoginService.create(name='github')
|
LoginService.create(name='github')
|
||||||
LoginService.create(name='quayrobot')
|
LoginService.create(name='quayrobot')
|
||||||
LoginService.create(name='ldap')
|
LoginService.create(name='ldap')
|
||||||
|
@ -212,7 +214,11 @@ def initialize_database():
|
||||||
|
|
||||||
LogEntryKind.create(name='org_create_team')
|
LogEntryKind.create(name='org_create_team')
|
||||||
LogEntryKind.create(name='org_delete_team')
|
LogEntryKind.create(name='org_delete_team')
|
||||||
|
LogEntryKind.create(name='org_invite_team_member')
|
||||||
|
LogEntryKind.create(name='org_delete_team_member_invite')
|
||||||
LogEntryKind.create(name='org_add_team_member')
|
LogEntryKind.create(name='org_add_team_member')
|
||||||
|
LogEntryKind.create(name='org_team_member_invite_accepted')
|
||||||
|
LogEntryKind.create(name='org_team_member_invite_declined')
|
||||||
LogEntryKind.create(name='org_remove_team_member')
|
LogEntryKind.create(name='org_remove_team_member')
|
||||||
LogEntryKind.create(name='org_set_team_description')
|
LogEntryKind.create(name='org_set_team_description')
|
||||||
LogEntryKind.create(name='org_set_team_role')
|
LogEntryKind.create(name='org_set_team_role')
|
||||||
|
@ -229,13 +235,15 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='delete_application')
|
LogEntryKind.create(name='delete_application')
|
||||||
LogEntryKind.create(name='reset_application_client_secret')
|
LogEntryKind.create(name='reset_application_client_secret')
|
||||||
|
|
||||||
# Note: These are deprecated.
|
# Note: These next two are deprecated.
|
||||||
LogEntryKind.create(name='add_repo_webhook')
|
LogEntryKind.create(name='add_repo_webhook')
|
||||||
LogEntryKind.create(name='delete_repo_webhook')
|
LogEntryKind.create(name='delete_repo_webhook')
|
||||||
|
|
||||||
LogEntryKind.create(name='add_repo_notification')
|
LogEntryKind.create(name='add_repo_notification')
|
||||||
LogEntryKind.create(name='delete_repo_notification')
|
LogEntryKind.create(name='delete_repo_notification')
|
||||||
|
|
||||||
|
LogEntryKind.create(name='regenerate_robot_token')
|
||||||
|
|
||||||
ImageStorageLocation.create(name='local_eu')
|
ImageStorageLocation.create(name='local_eu')
|
||||||
ImageStorageLocation.create(name='local_us')
|
ImageStorageLocation.create(name='local_us')
|
||||||
|
|
||||||
|
@ -251,6 +259,10 @@ def initialize_database():
|
||||||
ExternalNotificationMethod.create(name='email')
|
ExternalNotificationMethod.create(name='email')
|
||||||
ExternalNotificationMethod.create(name='webhook')
|
ExternalNotificationMethod.create(name='webhook')
|
||||||
|
|
||||||
|
ExternalNotificationMethod.create(name='flowdock')
|
||||||
|
ExternalNotificationMethod.create(name='hipchat')
|
||||||
|
ExternalNotificationMethod.create(name='slack')
|
||||||
|
|
||||||
NotificationKind.create(name='repo_push')
|
NotificationKind.create(name='repo_push')
|
||||||
NotificationKind.create(name='build_queued')
|
NotificationKind.create(name='build_queued')
|
||||||
NotificationKind.create(name='build_start')
|
NotificationKind.create(name='build_start')
|
||||||
|
@ -261,6 +273,7 @@ def initialize_database():
|
||||||
NotificationKind.create(name='over_private_usage')
|
NotificationKind.create(name='over_private_usage')
|
||||||
NotificationKind.create(name='expiring_license')
|
NotificationKind.create(name='expiring_license')
|
||||||
NotificationKind.create(name='maintenance')
|
NotificationKind.create(name='maintenance')
|
||||||
|
NotificationKind.create(name='org_team_invite')
|
||||||
|
|
||||||
NotificationKind.create(name='test_notification')
|
NotificationKind.create(name='test_notification')
|
||||||
|
|
||||||
|
@ -292,7 +305,7 @@ def populate_database():
|
||||||
new_user_2.verified = True
|
new_user_2.verified = True
|
||||||
new_user_2.save()
|
new_user_2.save()
|
||||||
|
|
||||||
new_user_3 = model.create_user('freshuser', 'password', 'no@thanks.com')
|
new_user_3 = model.create_user('freshuser', 'password', 'jschorr+test@devtable.com')
|
||||||
new_user_3.verified = True
|
new_user_3.verified = True
|
||||||
new_user_3.save()
|
new_user_3.save()
|
||||||
|
|
||||||
|
@ -313,7 +326,8 @@ def populate_database():
|
||||||
outside_org.verified = True
|
outside_org.verified = True
|
||||||
outside_org.save()
|
outside_org.save()
|
||||||
|
|
||||||
model.create_notification('test_notification', new_user_1, metadata={'some': 'value', 'arr': [1,2,3], 'obj': {'a': 1, 'b': 2}})
|
model.create_notification('test_notification', new_user_1,
|
||||||
|
metadata={'some':'value', 'arr':[1, 2, 3], 'obj':{'a':1, 'b':2}})
|
||||||
|
|
||||||
from_date = datetime.utcnow()
|
from_date = datetime.utcnow()
|
||||||
to_date = from_date + timedelta(hours=1)
|
to_date = from_date + timedelta(hours=1)
|
||||||
|
@ -377,18 +391,20 @@ def populate_database():
|
||||||
})
|
})
|
||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
repo = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
|
repo = 'ci.devtable.com:5000/%s/%s' % (building.namespace_user.username, building.name)
|
||||||
job_config = {
|
job_config = {
|
||||||
'repository': repo,
|
'repository': repo,
|
||||||
'docker_tags': ['latest'],
|
'docker_tags': ['latest'],
|
||||||
'build_subdir': '',
|
'build_subdir': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr@devtable.com')
|
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple',
|
||||||
|
'jschorr@devtable.com')
|
||||||
record.confirmed = True
|
record.confirmed = True
|
||||||
record.save()
|
record.save()
|
||||||
|
|
||||||
model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr+other@devtable.com')
|
model.create_email_authorization_for_repo(new_user_1.username, 'simple',
|
||||||
|
'jschorr+other@devtable.com')
|
||||||
|
|
||||||
build2 = model.create_repository_build(building, token, job_config,
|
build2 = model.create_repository_build(building, token, job_config,
|
||||||
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
||||||
|
@ -415,12 +431,12 @@ def populate_database():
|
||||||
|
|
||||||
model.create_robot('coolrobot', org)
|
model.create_robot('coolrobot', org)
|
||||||
|
|
||||||
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
|
oauth.create_application(org, 'Some Test App', 'http://localhost:8000',
|
||||||
client_id='deadbeef')
|
'http://localhost:8000/o2c.html', client_id='deadbeef')
|
||||||
|
|
||||||
oauth.create_application(org, 'Some Other Test App', 'http://quay.io', 'http://localhost:8000/o2c.html',
|
oauth.create_application(org, 'Some Other Test App', 'http://quay.io',
|
||||||
client_id='deadpork',
|
'http://localhost:8000/o2c.html', client_id='deadpork',
|
||||||
description = 'This is another test application')
|
description='This is another test application')
|
||||||
|
|
||||||
model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin')
|
model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin')
|
||||||
|
|
||||||
|
@ -442,8 +458,8 @@ def populate_database():
|
||||||
|
|
||||||
reader_team = model.create_team('readers', org, 'member',
|
reader_team = model.create_team('readers', org, 'member',
|
||||||
'Readers of orgrepo.')
|
'Readers of orgrepo.')
|
||||||
model.set_team_repo_permission(reader_team.name, org_repo.namespace,
|
model.set_team_repo_permission(reader_team.name, org_repo.namespace_user.username, org_repo.name,
|
||||||
org_repo.name, 'read')
|
'read')
|
||||||
model.add_user_to_team(new_user_2, reader_team)
|
model.add_user_to_team(new_user_2, reader_team)
|
||||||
model.add_user_to_team(reader, reader_team)
|
model.add_user_to_team(reader, reader_team)
|
||||||
|
|
||||||
|
@ -465,12 +481,9 @@ def populate_database():
|
||||||
(2, [], 'latest17'),
|
(2, [], 'latest17'),
|
||||||
(2, [], 'latest18'),])
|
(2, [], 'latest18'),])
|
||||||
|
|
||||||
model.add_prototype_permission(org, 'read', activating_user=new_user_1,
|
model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_user=new_user_2)
|
||||||
delegate_user=new_user_2)
|
model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_team=reader_team)
|
||||||
model.add_prototype_permission(org, 'read', activating_user=new_user_1,
|
model.add_prototype_permission(org, 'write', activating_user=new_user_2, delegate_user=new_user_1)
|
||||||
delegate_team=reader_team)
|
|
||||||
model.add_prototype_permission(org, 'write', activating_user=new_user_2,
|
|
||||||
delegate_user=new_user_1)
|
|
||||||
|
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
week_ago = today - timedelta(6)
|
week_ago = today - timedelta(6)
|
||||||
|
|
13
license.py
13
license.py
|
@ -1,13 +0,0 @@
|
||||||
import pickle
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
|
|
||||||
n = 24311791124264168943780535074639421876317270880681911499019414944027362498498429776192966738844514582251884695124256895677070273097239290537016363098432785034818859765271229653729724078304186025013011992335454557504431888746007324285000011384941749613875855493086506022340155196030616409545906383713728780211095701026770053812741971198465120292345817928060114890913931047021503727972067476586739126160044293621653486418983183727572502888923949587290840425930251185737996066354726953382305020440374552871209809125535533731995494145421279907938079885061852265339259634996180877443852561265066616143910755505151318370667L
|
|
||||||
e = 65537L
|
|
||||||
|
|
||||||
def load_license(license_path):
|
|
||||||
decryptor = RSA.construct((n, e))
|
|
||||||
with open(license_path, 'rb') as encrypted_license:
|
|
||||||
decrypted_data = decryptor.encrypt(encrypted_license.read(), 0)
|
|
||||||
|
|
||||||
return pickle.loads(decrypted_data[0])
|
|
BIN
license.pyc
BIN
license.pyc
Binary file not shown.
|
@ -32,5 +32,7 @@ raven
|
||||||
python-ldap
|
python-ldap
|
||||||
pycrypto
|
pycrypto
|
||||||
logentries
|
logentries
|
||||||
|
psycopg2
|
||||||
|
pyyaml
|
||||||
git+https://github.com/DevTable/aniso8601-fake.git
|
git+https://github.com/DevTable/aniso8601-fake.git
|
||||||
git+https://github.com/DevTable/anunidecode.git
|
git+https://github.com/DevTable/anunidecode.git
|
||||||
|
|
|
@ -12,6 +12,7 @@ Pillow==2.5.1
|
||||||
PyGithub==1.25.0
|
PyGithub==1.25.0
|
||||||
PyMySQL==0.6.2
|
PyMySQL==0.6.2
|
||||||
PyPDF2==1.22
|
PyPDF2==1.22
|
||||||
|
PyYAML==3.11
|
||||||
SQLAlchemy==0.9.7
|
SQLAlchemy==0.9.7
|
||||||
Werkzeug==0.9.6
|
Werkzeug==0.9.6
|
||||||
alembic==0.6.5
|
alembic==0.6.5
|
||||||
|
@ -44,6 +45,7 @@ python-dateutil==2.2
|
||||||
python-ldap==2.4.15
|
python-ldap==2.4.15
|
||||||
python-magic==0.4.6
|
python-magic==0.4.6
|
||||||
pytz==2014.4
|
pytz==2014.4
|
||||||
|
psycopg2==2.5.3
|
||||||
raven==5.0.0
|
raven==5.0.0
|
||||||
redis==2.10.1
|
redis==2.10.1
|
||||||
reportlab==2.7
|
reportlab==2.7
|
||||||
|
|
|
@ -21,8 +21,7 @@
|
||||||
|
|
||||||
|
|
||||||
#quay-logo {
|
#quay-logo {
|
||||||
width: 80px;
|
width: 100px;
|
||||||
margin-right: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#padding-container {
|
#padding-container {
|
||||||
|
@ -145,6 +144,15 @@ nav.navbar-default .navbar-nav>li>a.active {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-view-element .right-controls button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-view-element .message i.fa {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.notification-view-element .orginfo {
|
.notification-view-element .orginfo {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -464,6 +472,22 @@ i.toggle-icon:hover {
|
||||||
|
|
||||||
.docker-auth-dialog .token-dialog-body .well {
|
.docker-auth-dialog .token-dialog-body .well {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docker-auth-dialog .token-dialog-body .well i.fa-refresh {
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
right: 9px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: gray;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docker-auth-dialog .token-dialog-body .well i.fa-refresh:hover {
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docker-auth-dialog .token-view {
|
.docker-auth-dialog .token-view {
|
||||||
|
@ -729,7 +753,7 @@ i.toggle-icon:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-notification.notification-animated {
|
.user-notification.notification-animated {
|
||||||
width: 21px;
|
min-width: 21px;
|
||||||
|
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
-moz-transform: scale(0);
|
-moz-transform: scale(0);
|
||||||
|
@ -2257,6 +2281,14 @@ p.editable:hover i {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-box-element.disabled .input-group-addon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-box-element.disabled input {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.global-zeroclipboard-container embed {
|
.global-zeroclipboard-container embed {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -2535,7 +2567,7 @@ p.editable:hover i {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-build .build-log-error-element {
|
.repo-build .build-log-error-element .error-message-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
@ -2545,7 +2577,7 @@ p.editable:hover i {
|
||||||
margin-left: 22px;
|
margin-left: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-build .build-log-error-element i.fa {
|
.repo-build .build-log-error-element .error-message-container i.fa {
|
||||||
color: red;
|
color: red;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 13px;
|
top: 13px;
|
||||||
|
@ -3535,6 +3567,12 @@ p.editable:hover i {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tt-message {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tt-suggestion p {
|
.tt-suggestion p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -4226,7 +4264,7 @@ pre.command:before {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row.super-user td {
|
.user-row.super-user td {
|
||||||
background-color: #d9edf7;
|
background-color: #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row .user-class {
|
.user-row .user-class {
|
||||||
|
@ -4559,6 +4597,27 @@ i.quay-icon {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.flowdock-icon {
|
||||||
|
background-image: url(/static/img/flowdock.ico);
|
||||||
|
background-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.hipchat-icon {
|
||||||
|
background-image: url(/static/img/hipchat.png);
|
||||||
|
background-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.slack-icon {
|
||||||
|
background-image: url(/static/img/slack.ico);
|
||||||
|
background-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.external-notification-view-element {
|
.external-notification-view-element {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
@ -4593,4 +4652,68 @@ i.quay-icon {
|
||||||
|
|
||||||
.external-notification-view-element:hover .side-controls button {
|
.external-notification-view-element:hover .side-controls button {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .section-header {
|
||||||
|
color: #ccc;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .gravatar {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .entity-reference {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .invite-listing {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover {
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover.bottom-right .arrow:after {
|
||||||
|
border-bottom-color: #f7f7f7;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .team-view-add-element .help-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTriggerDialog .trigger-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTriggerDialog #runForm .field-title {
|
||||||
|
width: 120px;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
|
@ -1,4 +1,23 @@
|
||||||
<span bindonce class="build-log-error-element">
|
<div bindonce class="build-log-error-element">
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<span class="error-message-container">
|
||||||
<span class="error-message" bo-text="error.message"></span>
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
</span>
|
<span class="error-message" bo-text="error.message"></span>
|
||||||
|
<span ng-if="error.message == 'HTTP code: 403' && getLocalPullInfo().isLocal">
|
||||||
|
caused by attempting to pull private repository <a href="/repository/{{ getLocalPullInfo().repo }}">{{ getLocalPullInfo().repo }}</a>
|
||||||
|
<span ng-if="getLocalPullInfo().login">with inaccessible crdentials</span>
|
||||||
|
<span ng-if="!getLocalPullInfo().login">without credentials</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="alert alert-danger" ng-if="error.message == 'HTTP code: 403' && getLocalPullInfo().isLocal">
|
||||||
|
<div ng-if="getLocalPullInfo().login">
|
||||||
|
Note: The credentials <b>{{ getLocalPullInfo().login.username }}</b> for registry <b>{{ getLocalPullInfo().login.registry }}</b> cannot
|
||||||
|
access repository <a href="/repository/{{ getLocalPullInfo().repo }}">{{ getLocalPullInfo().repo }}</a>.
|
||||||
|
</div>
|
||||||
|
<div ng-if="!getLocalPullInfo().login">
|
||||||
|
Note: No robot account is specified for this build. Without such credentials, this pull will always fail. Please setup a new
|
||||||
|
build trigger with a robot account that has access to <a href="/repository/{{ getLocalPullInfo().repo }}">{{ getLocalPullInfo().repo }}</a> or make that repository public.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="copy-box-element">
|
<div class="copy-box-element" ng-class="disabled ? 'disabled' : ''">
|
||||||
<div class="id-container">
|
<div class="id-container">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<tr ng-if="currentMethod.fields.length"><td colspan="2"><hr></td></tr>
|
<tr ng-if="currentMethod.fields.length"><td colspan="2"><hr></td></tr>
|
||||||
|
|
||||||
<tr ng-repeat="field in currentMethod.fields">
|
<tr ng-repeat="field in currentMethod.fields">
|
||||||
<td>{{ field.title }}:</td>
|
<td valign="top">{{ field.title }}:</td>
|
||||||
<td>
|
<td>
|
||||||
<div ng-switch on="field.type">
|
<div ng-switch on="field.type">
|
||||||
<span ng-switch-when="email">
|
<span ng-switch-when="email">
|
||||||
|
@ -86,7 +86,11 @@
|
||||||
current-entity="currentConfig[field.name]"
|
current-entity="currentConfig[field.name]"
|
||||||
ng-model="currentConfig[field.name]"
|
ng-model="currentConfig[field.name]"
|
||||||
allowed-entities="['user', 'team', 'org']"
|
allowed-entities="['user', 'team', 'org']"
|
||||||
ng-switch-when="entity">
|
ng-switch-when="entity"></div>
|
||||||
|
|
||||||
|
<div ng-if="getHelpUrl(field, currentConfig)" style="margin-top: 10px">
|
||||||
|
See: <a href="{{ getHelpUrl(field, currentConfig) }}" target="_blank">{{ getHelpUrl(field, currentConfig) }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -10,19 +10,33 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body token-dialog-body">
|
<div class="modal-body token-dialog-body">
|
||||||
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
|
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
|
||||||
<div class="well well-sm">
|
|
||||||
|
<div class="well well-sm" ng-show="regenerating">
|
||||||
|
Regenerating Token...
|
||||||
|
<i class="fa fa-refresh fa-spin"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="well well-sm" ng-show="!regenerating">
|
||||||
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
|
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
|
||||||
|
<i class="fa fa-refresh" ng-show="supportsRegenerate" ng-click="askRegenerate()"
|
||||||
|
data-title="Regenerate Token"
|
||||||
|
data-placement="left"
|
||||||
|
bs-tooltip></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer" ng-show="regenerating">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" ng-show="!regenerating">
|
||||||
<span class="download-cfg" ng-show="isDownloadSupported()">
|
<span class="download-cfg" ng-show="isDownloadSupported()">
|
||||||
<i class="fa fa-download"></i>
|
<i class="fa fa-download"></i>
|
||||||
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
|
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
|
||||||
</span>
|
</span>
|
||||||
<div id="clipboardCopied" style="display: none">
|
<div class="clipboard-copied-message" style="display: none">
|
||||||
Copied to clipboard
|
Copied
|
||||||
</div>
|
</div>
|
||||||
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
|
<input type="hidden" name="command-data" id="command-data" value="{{ command }}">
|
||||||
|
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="command-data">Copy Login Command</button>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="current-item">
|
<div class="current-item">
|
||||||
<div class="dropdown-select-icon-transclude"></div>
|
<div class="dropdown-select-icon-transclude"></div>
|
||||||
<input type="text" class="lookahead-input form-control" placeholder="{{ placeholder }}"
|
<input type="text" class="lookahead-input form-control" placeholder="{{ placeholder }}"
|
||||||
ng-readonly="!lookaheadItems || !lookaheadItems.length"></input>
|
ng-readonly="!allowCustomInput"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||||
|
|
|
@ -7,15 +7,19 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind == 'org'">
|
<span ng-if="entity.kind == 'org'">
|
||||||
<img src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s=16&d=identicon">
|
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||||
<span class="entity-name">
|
<span class="entity-name">
|
||||||
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
||||||
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
||||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
<span ng-if="showGravatar != 'true' || !entity.gravatar">
|
||||||
|
<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>
|
||||||
|
|
||||||
<span class="entity-name" ng-if="entity.is_robot">
|
<span class="entity-name" ng-if="entity.is_robot">
|
||||||
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
||||||
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
ng-click="lazyLoad()">
|
ng-click="lazyLoad()">
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
|
<ul class="dropdown-menu" ng-class="pullRight == 'true' ? 'pull-right': ''" role="menu" aria-labelledby="entityDropdownMenu">
|
||||||
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
||||||
|
|
||||||
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
|
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
|
||||||
|
|
17
static/directives/external-login-button.html
Normal file
17
static/directives/external-login-button.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<span class="external-login-button-element">
|
||||||
|
<span ng-if="provider == 'github'">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
|
||||||
|
<i class="fa fa-github fa-lg"></i>
|
||||||
|
<span ng-if="action != 'attach'">Sign In with GitHub</span>
|
||||||
|
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-if="provider == 'google'">
|
||||||
|
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')" ng-disabled="signingIn">
|
||||||
|
<i class="fa fa-google fa-lg"></i>
|
||||||
|
<span ng-if="action != 'attach'">Sign In with Google</span>
|
||||||
|
<span ng-if="action == 'attach'">Attach to Google Account</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
|
@ -4,7 +4,7 @@
|
||||||
≡
|
≡
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||||
<img id="quay-logo" src="/static/img/black-horizontal.svg">
|
<img id="quay-logo" src="/static/img/quay-logo.png">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -37,15 +37,7 @@
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
<span class="badge user-notification notification-animated"
|
<span class="notifications-bubble"></span>
|
||||||
ng-show="notificationService.notifications.length"
|
|
||||||
ng-class="notificationService.notificationClasses"
|
|
||||||
bs-tooltip=""
|
|
||||||
data-title="User Notifications"
|
|
||||||
data-placement="left"
|
|
||||||
data-container="body">
|
|
||||||
{{ notificationService.notifications.length }}
|
|
||||||
</span>
|
|
||||||
<b class="caret"></b>
|
<b class="caret"></b>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
@ -58,11 +50,7 @@
|
||||||
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
||||||
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
||||||
Notifications
|
Notifications
|
||||||
<span class="badge user-notification"
|
<span class="notifications-bubble"></span>
|
||||||
ng-class="notificationService.notificationClasses"
|
|
||||||
ng-show="notificationService.notifications.length">
|
|
||||||
{{ notificationService.notifications.length }}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="container header">
|
<div class="container header">
|
||||||
<span class="header-text">
|
<span class="header-text">
|
||||||
<span ng-show="!performer">Usage Logs</span>
|
<span ng-show="!performer">Usage Logs</span>
|
||||||
<span class="entity-reference" name="performer.username" isrobot="performer.is_robot" ng-show="performer"></span>
|
<span class="entity-reference" entity="performer" ng-show="performer"></span>
|
||||||
<span id="logs-range" class="mini">
|
<span id="logs-range" class="mini">
|
||||||
From
|
From
|
||||||
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-max-date="{{ logEndDate }}" data-container="body" bs-datepicker/>
|
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-max-date="{{ logEndDate }}" data-container="body" bs-datepicker/>
|
||||||
|
|
38
static/directives/manual-trigger-build-dialog.html
Normal file
38
static/directives/manual-trigger-build-dialog.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="startTriggerDialog">
|
||||||
|
<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">Manully Start Build Trigger</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="trigger-description" trigger="trigger"></div>
|
||||||
|
|
||||||
|
<form name="runForm" id="runForm">
|
||||||
|
<table width="100%">
|
||||||
|
<tr ng-repeat="field in runParameters">
|
||||||
|
<td class="field-title" valign="top">{{ field.title }}:</td>
|
||||||
|
<td>
|
||||||
|
<div ng-switch on="field.type">
|
||||||
|
<span ng-switch-when="option">
|
||||||
|
<span class="quay-spinner" ng-show="!fieldOptions[field.name]"></span>
|
||||||
|
<select ng-model="parameters[field.name]" ng-show="fieldOptions[field.name]"
|
||||||
|
ng-options="value for value in fieldOptions[field.name]"
|
||||||
|
required>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" ng-model="parameters[field.name]" ng-switch-when="string" required>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-disabled="runForm.$invalid" ng-click="startTrigger()">Start Build</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
|
@ -3,7 +3,10 @@
|
||||||
<div class="aside-content">
|
<div class="aside-content">
|
||||||
<div class="aside-header">
|
<div class="aside-header">
|
||||||
<button type="button" class="close" ng-click="$hide()">×</button>
|
<button type="button" class="close" ng-click="$hide()">×</button>
|
||||||
<h4 class="aside-title">Notifications</h4>
|
<h4 class="aside-title">
|
||||||
|
Notifications
|
||||||
|
<span class="notifications-bubble"></span>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="aside-body">
|
<div class="aside-body">
|
||||||
<div ng-repeat="notification in notificationService.notifications">
|
<div ng-repeat="notification in notificationService.notifications">
|
||||||
|
|
|
@ -7,10 +7,13 @@
|
||||||
<span class="orgname">{{ notification.organization }}</span>
|
<span class="orgname">{{ notification.organization }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
|
||||||
<div class="right-controls">
|
<div class="right-controls">
|
||||||
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
||||||
Dismiss Notification
|
Dismiss Notification
|
||||||
</a>
|
</a>
|
||||||
|
<button class="btn" ng-class="'btn-' + action.kind" ng-repeat="action in getActions(notification)" ng-click="action.handler(notification)">
|
||||||
|
{{ action.title }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
7
static/directives/notifications-bubble.html
Normal file
7
static/directives/notifications-bubble.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<span class="notifications-bubble-element">
|
||||||
|
<span class="badge user-notification notification-animated"
|
||||||
|
ng-show="notificationService.notifications.length"
|
||||||
|
ng-class="notificationService.notificationClasses">
|
||||||
|
{{ notificationService.notifications.length }}<span ng-if="notificationService.additionalNotifications">+</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
|
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
|
||||||
shown="!!shownRobot" counter="showRobotCounter">
|
shown="!!shownRobot" counter="showRobotCounter" supports-regenerate="true" regenerate="regenerateToken(username)">
|
||||||
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
|
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue