Merge branch 'master' into comewithmeifyouwanttowork
This commit is contained in:
commit
3b72b26836
62 changed files with 923 additions and 714 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
|
|
@ -4,10 +4,10 @@ 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 # 21AUG2014
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
@ -4,10 +4,10 @@ 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 # 21AUG2014
|
||||||
|
|
||||||
# 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/
|
||||||
|
|
||||||
|
@ -38,9 +39,6 @@ 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
|
||||||
|
|
||||||
# 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
|
||||||
RUN venv/bin/python -m external_libraries
|
RUN venv/bin/python -m external_libraries
|
||||||
|
|
52
app.py
52
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
|
||||||
|
@ -21,11 +22,37 @@ from data.billing import Billing
|
||||||
from data.buildlogs import BuildLogs
|
from data.buildlogs import BuildLogs
|
||||||
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 +70,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)
|
||||||
|
|
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 _;
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,9 @@ 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
|
||||||
|
|
||||||
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'}],
|
||||||
|
|
|
@ -17,6 +17,8 @@ SCHEME_DRIVERS = {
|
||||||
'mysql': MySQLDatabase,
|
'mysql': MySQLDatabase,
|
||||||
'mysql+pymysql': MySQLDatabase,
|
'mysql+pymysql': MySQLDatabase,
|
||||||
'sqlite': SqliteDatabase,
|
'sqlite': SqliteDatabase,
|
||||||
|
'postgresql': PostgresqlDatabase,
|
||||||
|
'postgresql+psycopg2': PostgresqlDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
db = Proxy()
|
db = Proxy()
|
||||||
|
@ -32,7 +34,7 @@ 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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""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
|
||||||
|
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
||||||
|
from data.database import all_models
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
schema = gen_sqlalchemy_metadata(all_models)
|
||||||
|
|
||||||
|
op.bulk_insert(schema.tables['logentrykind'],
|
||||||
|
[
|
||||||
|
{'id': 41, 'name':'regenerate_robot_token'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
schema = gen_sqlalchemy_metadata(all_models)
|
||||||
|
|
||||||
|
logentrykind = schema.tables['logentrykind']
|
||||||
|
op.execute(
|
||||||
|
(logentrykind.delete()
|
||||||
|
.where(logentrykind.c.name == op.inline_literal('regenerate_robot_token')))
|
||||||
|
|
||||||
|
)
|
|
@ -20,12 +20,12 @@ def get_id(query):
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
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():
|
||||||
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))
|
||||||
|
|
|
@ -203,7 +203,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 +375,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'], ),
|
||||||
|
|
|
@ -76,8 +76,7 @@ class UserAlreadyInTeam(DataModelException):
|
||||||
|
|
||||||
|
|
||||||
def is_create_user_allowed():
|
def is_create_user_allowed():
|
||||||
return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT']
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, password, email):
|
def create_user(username, password, email):
|
||||||
""" Creates a regular user, if allowed. """
|
""" Creates a regular user, if allowed. """
|
||||||
|
@ -188,6 +187,19 @@ def create_robot(robot_shortname, parent):
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise DataModelException(ex.message)
|
raise DataModelException(ex.message)
|
||||||
|
|
||||||
|
def get_robot(robot_shortname, parent):
|
||||||
|
robot_username = format_robot_username(parent.username, robot_shortname)
|
||||||
|
robot = lookup_robot(robot_username)
|
||||||
|
|
||||||
|
if not robot:
|
||||||
|
msg = ('Could not find robot with username: %s' %
|
||||||
|
robot_username)
|
||||||
|
raise InvalidRobotException(msg)
|
||||||
|
|
||||||
|
service = LoginService.get(name='quayrobot')
|
||||||
|
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
||||||
|
|
||||||
|
return robot, login.service_ident
|
||||||
|
|
||||||
def lookup_robot(robot_username):
|
def lookup_robot(robot_username):
|
||||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||||
|
@ -198,7 +210,6 @@ def lookup_robot(robot_username):
|
||||||
|
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
|
|
||||||
def verify_robot(robot_username, password):
|
def verify_robot(robot_username, password):
|
||||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||||
found = list(joined.where(FederatedLogin.service_ident == password,
|
found = list(joined.where(FederatedLogin.service_ident == password,
|
||||||
|
@ -211,6 +222,25 @@ def verify_robot(robot_username, password):
|
||||||
|
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
|
def regenerate_robot_token(robot_shortname, parent):
|
||||||
|
robot_username = format_robot_username(parent.username, robot_shortname)
|
||||||
|
|
||||||
|
robot = lookup_robot(robot_username)
|
||||||
|
if not robot:
|
||||||
|
raise InvalidRobotException('Could not find robot with username: %s' %
|
||||||
|
robot_username)
|
||||||
|
|
||||||
|
password = random_string_generator(length=64)()
|
||||||
|
robot.email = password
|
||||||
|
|
||||||
|
service = LoginService.get(name='quayrobot')
|
||||||
|
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
||||||
|
login.service_ident = password
|
||||||
|
|
||||||
|
login.save()
|
||||||
|
robot.save()
|
||||||
|
|
||||||
|
return robot, password
|
||||||
|
|
||||||
def delete_robot(robot_username):
|
def delete_robot(robot_username):
|
||||||
try:
|
try:
|
||||||
|
@ -872,6 +902,34 @@ def get_all_repo_users(namespace_name, repository_name):
|
||||||
Repository.name == repository_name)
|
Repository.name == repository_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_repo_users_transitive_via_teams(namespace_name, repository_name):
|
||||||
|
select = User.select().distinct()
|
||||||
|
with_team_member = select.join(TeamMember)
|
||||||
|
with_team = with_team_member.join(Team)
|
||||||
|
with_perm = with_team.join(RepositoryPermission)
|
||||||
|
with_repo = with_perm.join(Repository)
|
||||||
|
return with_repo.where(Repository.namespace == namespace_name,
|
||||||
|
Repository.name == repository_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_repo_users_transitive(namespace_name, repository_name):
|
||||||
|
# Load the users found via teams and directly via permissions.
|
||||||
|
via_teams = get_all_repo_users_transitive_via_teams(namespace_name, repository_name)
|
||||||
|
directly = [perm.user for perm in get_all_repo_users(namespace_name, repository_name)]
|
||||||
|
|
||||||
|
# Filter duplicates.
|
||||||
|
user_set = set()
|
||||||
|
|
||||||
|
def check_add(u):
|
||||||
|
if u.username in user_set:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_set.add(u.username)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return [user for user in list(directly) + list(via_teams) if check_add(user)]
|
||||||
|
|
||||||
|
|
||||||
def get_repository_for_resource(resource_key):
|
def get_repository_for_resource(resource_key):
|
||||||
try:
|
try:
|
||||||
return (Repository
|
return (Repository
|
||||||
|
@ -1706,19 +1764,20 @@ def create_notification(kind_name, target, metadata={}):
|
||||||
|
|
||||||
def create_unique_notification(kind_name, target, metadata={}):
|
def create_unique_notification(kind_name, target, metadata={}):
|
||||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
if list_notifications(target, kind_name).count() == 0:
|
if list_notifications(target, kind_name, limit=1).count() == 0:
|
||||||
create_notification(kind_name, target, metadata)
|
create_notification(kind_name, target, metadata)
|
||||||
|
|
||||||
|
|
||||||
def lookup_notification(user, uuid):
|
def lookup_notification(user, uuid):
|
||||||
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True))
|
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True, limit=1))
|
||||||
if not results:
|
if not results:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return results[0]
|
return results[0]
|
||||||
|
|
||||||
|
|
||||||
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False):
|
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False,
|
||||||
|
page=None, limit=None):
|
||||||
Org = User.alias()
|
Org = User.alias()
|
||||||
AdminTeam = Team.alias()
|
AdminTeam = Team.alias()
|
||||||
AdminTeamMember = TeamMember.alias()
|
AdminTeamMember = TeamMember.alias()
|
||||||
|
@ -1756,6 +1815,11 @@ def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=F
|
||||||
.switch(Notification)
|
.switch(Notification)
|
||||||
.where(Notification.uuid == id_filter))
|
.where(Notification.uuid == id_filter))
|
||||||
|
|
||||||
|
if page:
|
||||||
|
query = query.paginate(page, limit)
|
||||||
|
elif limit:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
require_user_admin, show_if, hide_if, 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
|
||||||
|
@ -23,7 +23,11 @@ def get_card(user):
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
|
try:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
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
|
||||||
|
@ -46,7 +50,11 @@ def get_card(user):
|
||||||
|
|
||||||
def set_card(user, token):
|
def set_card(user, token):
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
|
try:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
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
|
||||||
|
@ -55,6 +63,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)
|
||||||
|
|
||||||
|
@ -75,7 +85,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
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]
|
||||||
}
|
}
|
||||||
|
@ -228,7 +242,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:
|
||||||
|
try:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
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)
|
||||||
|
@ -291,7 +308,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:
|
||||||
|
try:
|
||||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
try:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
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
|
||||||
|
try:
|
||||||
cus.cancel_subscription()
|
cus.cancel_subscription()
|
||||||
cus.save()
|
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)
|
||||||
|
|
|
@ -42,24 +42,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,
|
||||||
|
|
|
@ -350,8 +350,8 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
(robot_namespace, shortname) = parse_robot_username(user.username)
|
(robot_namespace, shortname) = parse_robot_username(user.username)
|
||||||
return AdministerOrganizationPermission(robot_namespace).can()
|
return AdministerOrganizationPermission(robot_namespace).can()
|
||||||
|
|
||||||
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
|
repo_users = list(model.get_all_repo_users_transitive(base_namespace, base_repository))
|
||||||
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
|
read_robots = [robot_view(user) for user in repo_users if is_valid_robot(user)]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'namespace': base_namespace,
|
'namespace': base_namespace,
|
||||||
|
|
|
@ -7,8 +7,9 @@ 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,
|
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,
|
||||||
|
license_error)
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -403,11 +404,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -413,8 +413,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 + '/' + 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, 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')
|
||||||
|
|
|
@ -184,6 +184,8 @@ class BuildFailureEvent(NotificationEvent):
|
||||||
return 'build_failure'
|
return 'build_failure'
|
||||||
|
|
||||||
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',
|
||||||
|
|
|
@ -370,6 +370,7 @@ def generate_ancestry(image_id, uuid, locations, parent_id=None, parent_uuid=Non
|
||||||
if not parent_id:
|
if not parent_id:
|
||||||
store.put_content(locations, store.image_ancestry_path(uuid), json.dumps([image_id]))
|
store.put_content(locations, store.image_ancestry_path(uuid), json.dumps([image_id]))
|
||||||
return
|
return
|
||||||
|
|
||||||
data = store.get_content(parent_locations, store.image_ancestry_path(parent_uuid))
|
data = store.get_content(parent_locations, store.image_ancestry_path(parent_uuid))
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
data.insert(0, image_id)
|
data.insert(0, image_id)
|
||||||
|
@ -470,8 +471,13 @@ def put_image_json(namespace, repository, image_id):
|
||||||
store.put_content(repo_image.storage.locations, json_path, request.data)
|
store.put_content(repo_image.storage.locations, json_path, request.data)
|
||||||
|
|
||||||
profile.debug('Generating image ancestry')
|
profile.debug('Generating image ancestry')
|
||||||
|
|
||||||
|
try:
|
||||||
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
||||||
parent_locations)
|
parent_locations)
|
||||||
|
except IOError as ioe:
|
||||||
|
profile.debug('Error when generating ancestry: %s' % ioe.message)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
profile.debug('Done')
|
profile.debug('Done')
|
||||||
return make_response('true', 200)
|
return make_response('true', 200)
|
||||||
|
|
|
@ -232,13 +232,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')
|
||||||
|
|
||||||
|
|
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
|
||||||
|
|
|
@ -473,6 +473,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 {
|
||||||
|
@ -738,7 +754,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);
|
||||||
|
@ -832,7 +848,7 @@ i.toggle-icon:hover {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-icon.waiting, .phase-icon.starting, .phase-icon.initializing {
|
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2266,6 +2282,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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,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
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>
|
||||||
|
|
347
static/js/app.js
347
static/js/app.js
|
@ -1,6 +1,46 @@
|
||||||
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||||||
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
|
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
|
||||||
|
|
||||||
|
$.fn.clipboardCopy = function() {
|
||||||
|
if (zeroClipboardSupported) {
|
||||||
|
(new ZeroClipboard($(this)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
var zeroClipboardSupported = true;
|
||||||
|
ZeroClipboard.config({
|
||||||
|
'swfPath': 'static/lib/ZeroClipboard.swf'
|
||||||
|
});
|
||||||
|
|
||||||
|
ZeroClipboard.on("error", function(e) {
|
||||||
|
zeroClipboardSupported = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ZeroClipboard.on('aftercopy', function(e) {
|
||||||
|
var container = e.target.parentNode.parentNode.parentNode;
|
||||||
|
var message = $(container).find('.clipboard-copied-message')[0];
|
||||||
|
|
||||||
|
// Resets the animation.
|
||||||
|
var elem = message;
|
||||||
|
elem.style.display = 'none';
|
||||||
|
elem.classList.remove('animated');
|
||||||
|
|
||||||
|
// Show the notification.
|
||||||
|
setTimeout(function() {
|
||||||
|
elem.style.display = 'inline-block';
|
||||||
|
elem.classList.add('animated');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Reset the notification.
|
||||||
|
setTimeout(function() {
|
||||||
|
elem.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
function getRestUrl(args) {
|
function getRestUrl(args) {
|
||||||
var url = '';
|
var url = '';
|
||||||
for (var i = 0; i < arguments.length; ++i) {
|
for (var i = 0; i < arguments.length; ++i) {
|
||||||
|
@ -59,18 +99,8 @@ function getFirstTextLine(commentString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
|
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
|
||||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
|
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
|
||||||
bootbox.dialog({
|
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
|
||||||
"message": resp.data ? resp.data['message'] : 'The robot account could not be created',
|
|
||||||
"title": "Cannot create robot account",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
||||||
|
@ -84,18 +114,8 @@ function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
||||||
'teamname': teamname
|
'teamname': teamname
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.updateOrganizationTeam(data, params).then(callback, function(resp) {
|
ApiService.updateOrganizationTeam(data, params)
|
||||||
bootbox.dialog({
|
.then(callback, ApiService.errorDisplay('Cannot create team'));
|
||||||
"message": resp.data ? resp.data : 'The team could not be created',
|
|
||||||
"title": "Cannot create team",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarkedDown(string) {
|
function getMarkedDown(string) {
|
||||||
|
@ -870,6 +890,38 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
buildMethodsForEndpointResource(endpointResource, resourceMap);
|
buildMethodsForEndpointResource(endpointResource, resourceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiService.getErrorMessage = function(resp, defaultMessage) {
|
||||||
|
var message = defaultMessage;
|
||||||
|
if (resp['data']) {
|
||||||
|
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
apiService.errorDisplay = function(defaultMessage, opt_handler) {
|
||||||
|
return function(resp) {
|
||||||
|
var message = apiService.getErrorMessage(resp, defaultMessage);
|
||||||
|
if (opt_handler) {
|
||||||
|
var handlerMessage = opt_handler(resp);
|
||||||
|
if (handlerMessage) {
|
||||||
|
message = handlerMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": message,
|
||||||
|
"title": defaultMessage,
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return apiService;
|
return apiService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
@ -1126,7 +1178,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'user': null,
|
'user': null,
|
||||||
'notifications': [],
|
'notifications': [],
|
||||||
'notificationClasses': [],
|
'notificationClasses': [],
|
||||||
'notificationSummaries': []
|
'notificationSummaries': [],
|
||||||
|
'additionalNotifications': false
|
||||||
};
|
};
|
||||||
|
|
||||||
var pollTimerHandle = null;
|
var pollTimerHandle = null;
|
||||||
|
@ -1244,7 +1297,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'uuid': notification.id
|
'uuid': notification.id
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.updateUserNotification(notification, params);
|
ApiService.updateUserNotification(notification, params, function() {
|
||||||
|
notificationService.update();
|
||||||
|
}, ApiService.errorDisplay('Could not update notification'));
|
||||||
|
|
||||||
var index = $.inArray(notification, notificationService.notifications);
|
var index = $.inArray(notification, notificationService.notifications);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
@ -1310,6 +1365,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
|
|
||||||
ApiService.listUserNotifications().then(function(resp) {
|
ApiService.listUserNotifications().then(function(resp) {
|
||||||
notificationService.notifications = resp['notifications'];
|
notificationService.notifications = resp['notifications'];
|
||||||
|
notificationService.additionalNotifications = resp['additional'];
|
||||||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1541,7 +1597,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.changePlan = function($scope, orgname, planId, callbacks) {
|
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
|
||||||
if (!Features.BILLING) { return; }
|
if (!Features.BILLING) { return; }
|
||||||
|
|
||||||
if (callbacks['started']) {
|
if (callbacks['started']) {
|
||||||
|
@ -1554,7 +1610,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
planService.getCardInfo(orgname, function(cardInfo) {
|
planService.getCardInfo(orgname, function(cardInfo) {
|
||||||
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
||||||
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
|
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
|
||||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title);
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1627,9 +1683,34 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return email;
|
return email;
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title) {
|
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
|
||||||
if (!Features.BILLING) { return; }
|
if (!Features.BILLING) { return; }
|
||||||
|
|
||||||
|
// If the async parameter is true and this is a browser that does not allow async popup of the
|
||||||
|
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
|
||||||
|
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
|
||||||
|
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
|
||||||
|
|
||||||
|
if (opt_async && (isIE || isMobileSafari)) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": "Please click 'Subscribe' to continue",
|
||||||
|
"buttons": {
|
||||||
|
"subscribe": {
|
||||||
|
"label": "Subscribe",
|
||||||
|
"className": "btn-primary",
|
||||||
|
"callback": function() {
|
||||||
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"close": {
|
||||||
|
"label": "Cancel",
|
||||||
|
"className": "btn-default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (callbacks['opening']) {
|
if (callbacks['opening']) {
|
||||||
callbacks['opening']();
|
callbacks['opening']();
|
||||||
}
|
}
|
||||||
|
@ -2084,18 +2165,7 @@ quayApp.directive('applicationReference', function () {
|
||||||
template: '/static/directives/application-reference-dialog.html',
|
template: '/static/directives/application-reference-dialog.html',
|
||||||
show: true
|
show: true
|
||||||
});
|
});
|
||||||
}, function() {
|
}, ApiService.errorDisplay('Application could not be found'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": 'The application could not be found; it might have been deleted.',
|
|
||||||
"title": "Cannot find application",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2176,6 +2246,8 @@ quayApp.directive('copyBox', function () {
|
||||||
'hoveringMessage': '=hoveringMessage'
|
'hoveringMessage': '=hoveringMessage'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $rootScope) {
|
controller: function($scope, $element, $rootScope) {
|
||||||
|
$scope.disabled = false;
|
||||||
|
|
||||||
var number = $rootScope.__copyBoxIdCounter || 0;
|
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||||
$rootScope.__copyBoxIdCounter = number + 1;
|
$rootScope.__copyBoxIdCounter = number + 1;
|
||||||
$scope.inputId = "copy-box-input-" + number;
|
$scope.inputId = "copy-box-input-" + number;
|
||||||
|
@ -2185,27 +2257,7 @@ quayApp.directive('copyBox', function () {
|
||||||
|
|
||||||
input.attr('id', $scope.inputId);
|
input.attr('id', $scope.inputId);
|
||||||
button.attr('data-clipboard-target', $scope.inputId);
|
button.attr('data-clipboard-target', $scope.inputId);
|
||||||
|
$scope.disabled = !button.clipboardCopy();
|
||||||
var clip = new ZeroClipboard($(button), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
|
||||||
clip.on('complete', function(e) {
|
|
||||||
var message = $(this.parentNode.parentNode.parentNode).find('.clipboard-copied-message')[0];
|
|
||||||
|
|
||||||
// Resets the animation.
|
|
||||||
var elem = message;
|
|
||||||
elem.style.display = 'none';
|
|
||||||
elem.classList.remove('animated');
|
|
||||||
|
|
||||||
// Show the notification.
|
|
||||||
setTimeout(function() {
|
|
||||||
elem.style.display = 'inline-block';
|
|
||||||
elem.classList.add('animated');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// Reset the notification.
|
|
||||||
setTimeout(function() {
|
|
||||||
elem.style.display = 'none';
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
@ -2439,10 +2491,37 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
||||||
'username': '=username',
|
'username': '=username',
|
||||||
'token': '=token',
|
'token': '=token',
|
||||||
'shown': '=shown',
|
'shown': '=shown',
|
||||||
'counter': '=counter'
|
'counter': '=counter',
|
||||||
|
'supportsRegenerate': '@supportsRegenerate',
|
||||||
|
'regenerate': '®enerate'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
|
var updateCommand = function() {
|
||||||
|
$scope.command = 'docker login -e="." -u="' + $scope.username +
|
||||||
|
'" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('username', updateCommand);
|
||||||
|
$scope.$watch('token', updateCommand);
|
||||||
|
|
||||||
|
$scope.regenerating = true;
|
||||||
|
|
||||||
|
$scope.askRegenerate = function() {
|
||||||
|
bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
|
||||||
|
if (resp) {
|
||||||
|
$scope.regenerating = true;
|
||||||
|
$scope.regenerate({'username': $scope.username, 'token': $scope.token});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.isDownloadSupported = function() {
|
$scope.isDownloadSupported = function() {
|
||||||
|
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||||
|
if (isSafari) {
|
||||||
|
// Doesn't work properly in Safari, sadly.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try { return !!new Blob(); } catch(e) {}
|
try { return !!new Blob(); } catch(e) {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -2461,6 +2540,8 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
||||||
};
|
};
|
||||||
|
|
||||||
var show = function(r) {
|
var show = function(r) {
|
||||||
|
$scope.regenerating = false;
|
||||||
|
|
||||||
if (!$scope.shown || !$scope.username || !$scope.token) {
|
if (!$scope.shown || !$scope.username || !$scope.token) {
|
||||||
$('#dockerauthmodal').modal('hide');
|
$('#dockerauthmodal').modal('hide');
|
||||||
return;
|
return;
|
||||||
|
@ -2706,6 +2787,8 @@ quayApp.directive('logsView', function () {
|
||||||
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
||||||
|
|
||||||
// Note: These are deprecated.
|
// Note: These are deprecated.
|
||||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||||
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||||
|
@ -2752,6 +2835,7 @@ quayApp.directive('logsView', function () {
|
||||||
'reset_application_client_secret': 'Reset Client Secret',
|
'reset_application_client_secret': 'Reset Client Secret',
|
||||||
'add_repo_notification': 'Add repository notification',
|
'add_repo_notification': 'Add repository notification',
|
||||||
'delete_repo_notification': 'Delete repository notification',
|
'delete_repo_notification': 'Delete repository notification',
|
||||||
|
'regenerate_robot_token': 'Regenerate Robot Token',
|
||||||
|
|
||||||
// Note: these are deprecated.
|
// Note: these are deprecated.
|
||||||
'add_repo_webhook': 'Add webhook',
|
'add_repo_webhook': 'Add webhook',
|
||||||
|
@ -2878,18 +2962,7 @@ quayApp.directive('applicationManager', function () {
|
||||||
|
|
||||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||||
$scope.applications.push(resp);
|
$scope.applications.push(resp);
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Cannot create application'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp['message'] || 'The application could not be created',
|
|
||||||
"title": "Cannot create application",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var update = function() {
|
var update = function() {
|
||||||
|
@ -2934,6 +3007,20 @@ quayApp.directive('robotsManager', function () {
|
||||||
$scope.shownRobot = null;
|
$scope.shownRobot = null;
|
||||||
$scope.showRobotCounter = 0;
|
$scope.showRobotCounter = 0;
|
||||||
|
|
||||||
|
$scope.regenerateToken = function(username) {
|
||||||
|
if (!username) { return; }
|
||||||
|
|
||||||
|
var shortName = $scope.getShortenedName(username);
|
||||||
|
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
|
||||||
|
var index = $scope.findRobotIndexByName(username);
|
||||||
|
if (index >= 0) {
|
||||||
|
$scope.robots.splice(index, 1);
|
||||||
|
$scope.robots.push(updated);
|
||||||
|
}
|
||||||
|
$scope.shownRobot = updated;
|
||||||
|
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showRobot = function(info) {
|
$scope.showRobot = function(info) {
|
||||||
$scope.shownRobot = info;
|
$scope.shownRobot = info;
|
||||||
$scope.showRobotCounter++;
|
$scope.showRobotCounter++;
|
||||||
|
@ -2974,18 +3061,7 @@ quayApp.directive('robotsManager', function () {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
$scope.robots.splice(index, 1);
|
$scope.robots.splice(index, 1);
|
||||||
}
|
}
|
||||||
}, function() {
|
}, ApiService.errorDisplay('Cannot delete robot account'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": 'The selected robot account could not be deleted',
|
|
||||||
"title": "Cannot delete robot account",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var update = function() {
|
var update = function() {
|
||||||
|
@ -3050,18 +3126,7 @@ quayApp.directive('prototypeManager', function () {
|
||||||
|
|
||||||
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
|
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||||
prototype.role = role;
|
prototype.role = role;
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Cannot modify permission'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data : 'The permission could not be modified',
|
|
||||||
"title": "Cannot modify permission",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.comparePrototypes = function(p) {
|
$scope.comparePrototypes = function(p) {
|
||||||
|
@ -3101,23 +3166,16 @@ quayApp.directive('prototypeManager', function () {
|
||||||
data['activating_user'] = $scope.activatingForNew;
|
data['activating_user'] = $scope.activatingForNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot create permission',
|
||||||
|
function(resp) {
|
||||||
|
$('#addPermissionDialogModal').modal('hide');
|
||||||
|
});
|
||||||
|
|
||||||
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
|
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||||
$scope.prototypes.push(resp);
|
$scope.prototypes.push(resp);
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
$('#addPermissionDialogModal').modal('hide');
|
$('#addPermissionDialogModal').modal('hide');
|
||||||
}, function(resp) {
|
}, errorHandler);
|
||||||
$('#addPermissionDialogModal').modal('hide');
|
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data : 'The permission could not be created',
|
|
||||||
"title": "Cannot create permission",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deletePrototype = function(prototype) {
|
$scope.deletePrototype = function(prototype) {
|
||||||
|
@ -3131,18 +3189,7 @@ quayApp.directive('prototypeManager', function () {
|
||||||
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
|
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
|
||||||
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
|
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Cannot delete permission'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data : 'The permission could not be deleted',
|
|
||||||
"title": "Cannot delete permission",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var update = function() {
|
var update = function() {
|
||||||
|
@ -3990,7 +4037,7 @@ quayApp.directive('planManager', function () {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.changeSubscription = function(planId) {
|
$scope.changeSubscription = function(planId, opt_async) {
|
||||||
if ($scope.planChanging) { return; }
|
if ($scope.planChanging) { return; }
|
||||||
|
|
||||||
var callbacks = {
|
var callbacks = {
|
||||||
|
@ -4004,7 +4051,7 @@ quayApp.directive('planManager', function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
|
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.cancelSubscription = function() {
|
$scope.cancelSubscription = function() {
|
||||||
|
@ -4067,7 +4114,7 @@ quayApp.directive('planManager', function () {
|
||||||
if ($scope.readyForPlan) {
|
if ($scope.readyForPlan) {
|
||||||
var planRequested = $scope.readyForPlan();
|
var planRequested = $scope.readyForPlan();
|
||||||
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
||||||
$scope.changeSubscription(planRequested);
|
$scope.changeSubscription(planRequested, /* async */true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4485,26 +4532,17 @@ quayApp.directive('setupTriggerDialog', function () {
|
||||||
|
|
||||||
$scope.activating = true;
|
$scope.activating = true;
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
|
||||||
|
$scope.hide();
|
||||||
|
$scope.canceled({'trigger': $scope.trigger});
|
||||||
|
});
|
||||||
|
|
||||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||||
$scope.hide();
|
$scope.hide();
|
||||||
$scope.trigger['is_active'] = true;
|
$scope.trigger['is_active'] = true;
|
||||||
$scope.trigger['pull_robot'] = resp['pull_robot'];
|
$scope.trigger['pull_robot'] = resp['pull_robot'];
|
||||||
$scope.activated({'trigger': $scope.trigger});
|
$scope.activated({'trigger': $scope.trigger});
|
||||||
}, function(resp) {
|
}, errorHandler);
|
||||||
$scope.hide();
|
|
||||||
$scope.canceled({'trigger': $scope.trigger});
|
|
||||||
|
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
|
||||||
"title": "Could not activate build trigger",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var check = function() {
|
var check = function() {
|
||||||
|
@ -4844,6 +4882,9 @@ quayApp.directive('buildMessage', function () {
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
return 'Waiting for available build worker';
|
return 'Waiting for available build worker';
|
||||||
|
|
||||||
|
case 'unpacking':
|
||||||
|
return 'Unpacking build package';
|
||||||
|
|
||||||
case 'pulling':
|
case 'pulling':
|
||||||
return 'Pulling base image';
|
return 'Pulling base image';
|
||||||
|
|
||||||
|
@ -4899,6 +4940,7 @@ quayApp.directive('buildProgress', function () {
|
||||||
case 'starting':
|
case 'starting':
|
||||||
case 'waiting':
|
case 'waiting':
|
||||||
case 'cannot_load':
|
case 'cannot_load':
|
||||||
|
case 'unpacking':
|
||||||
return 0;
|
return 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -5118,6 +5160,23 @@ quayApp.directive('twitterView', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('notificationsBubble', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/notifications-bubble.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
},
|
||||||
|
controller: function($scope, UserService, NotificationService) {
|
||||||
|
$scope.notificationService = NotificationService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('notificationView', function () {
|
quayApp.directive('notificationView', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
@ -5705,8 +5764,8 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Features.BILLING && response.status == 402) {
|
if (response.status == 503) {
|
||||||
$('#overlicenseModal').modal({});
|
$('#cannotContactService').modal({});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,4 @@
|
||||||
$.fn.clipboardCopy = function() {
|
|
||||||
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
|
||||||
|
|
||||||
clip.on('complete', function() {
|
|
||||||
// Resets the animation.
|
|
||||||
var elem = $('#clipboardCopied')[0];
|
|
||||||
if (!elem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.style.display = 'none';
|
|
||||||
elem.classList.remove('animated');
|
|
||||||
|
|
||||||
// Show the notification.
|
|
||||||
setTimeout(function() {
|
|
||||||
if (!elem) { return; }
|
|
||||||
elem.style.display = 'inline-block';
|
|
||||||
elem.classList.add('animated');
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function SignInCtrl($scope, $location) {
|
function SignInCtrl($scope, $location) {
|
||||||
var redirect = $location.search()['redirect'];
|
|
||||||
if (redirect && redirect.indexOf('/') < 0) {
|
|
||||||
delete $location.search()['redirect'];
|
|
||||||
$scope.redirectUrl = '/' + redirect;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.redirectUrl = '/';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GuideCtrl() {
|
function GuideCtrl() {
|
||||||
|
@ -543,23 +513,15 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
'image': image.id
|
'image': image.id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) {
|
||||||
|
$('#addTagModal').modal('hide');
|
||||||
|
});
|
||||||
|
|
||||||
ApiService.changeTagImage(data, params).then(function(resp) {
|
ApiService.changeTagImage(data, params).then(function(resp) {
|
||||||
$scope.creatingTag = false;
|
$scope.creatingTag = false;
|
||||||
loadViewInfo();
|
loadViewInfo();
|
||||||
$('#addTagModal').modal('hide');
|
$('#addTagModal').modal('hide');
|
||||||
}, function(resp) {
|
}, errorHandler);
|
||||||
$('#addTagModal').modal('hide');
|
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data : 'Could not create or move tag',
|
|
||||||
"title": "Cannot create or move tag",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteTag = function(tagName) {
|
$scope.deleteTag = function(tagName) {
|
||||||
|
@ -573,18 +535,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
ApiService.deleteFullTag(null, params).then(function() {
|
ApiService.deleteFullTag(null, params).then(function() {
|
||||||
loadViewInfo();
|
loadViewInfo();
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Cannot delete tag'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data : 'Could not delete tag',
|
|
||||||
"title": "Cannot delete tag",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getImagesForTagBySize = function(tag) {
|
$scope.getImagesForTagBySize = function(tag) {
|
||||||
|
@ -763,8 +714,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
|
|
||||||
// Load the builds for this repository. If none are active it will cancel the poll.
|
// Load the builds for this repository. If none are active it will cancel the poll.
|
||||||
startBuildInfoTimer(repo);
|
startBuildInfoTimer(repo);
|
||||||
|
|
||||||
$('#copyClipboard').clipboardCopy();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1373,17 +1322,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteRole = function(entityName, kind) {
|
$scope.deleteRole = function(entityName, kind) {
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot change permission', function(resp) {
|
||||||
|
if (resp.status == 409) {
|
||||||
|
return 'Cannot change permission as you do not have the authority';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var permissionDelete = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
var permissionDelete = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||||
permissionDelete.customDELETE().then(function() {
|
permissionDelete.customDELETE().then(function() {
|
||||||
delete $scope.permissions[kind][entityName];
|
delete $scope.permissions[kind][entityName];
|
||||||
}, function(resp) {
|
}, errorHandler);
|
||||||
if (resp.status == 409) {
|
|
||||||
$scope.changePermError = resp.data || '';
|
|
||||||
$('#channgechangepermModal').modal({});
|
|
||||||
} else {
|
|
||||||
$('#cannotchangeModal').modal({});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addRole = function(entityName, role, kind) {
|
$scope.addRole = function(entityName, role, kind) {
|
||||||
|
@ -1394,9 +1342,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||||
permissionPost.customPUT(permission).then(function(result) {
|
permissionPost.customPUT(permission).then(function(result) {
|
||||||
$scope.permissions[kind][entityName] = result;
|
$scope.permissions[kind][entityName] = result;
|
||||||
}, function(result) {
|
}, ApiService.errorDisplay('Cannot change permission'));
|
||||||
$('#cannotchangeModal').modal({});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.roles = [
|
$scope.roles = [
|
||||||
|
@ -1611,18 +1557,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
window.console.log(resp);
|
window.console.log(resp);
|
||||||
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
||||||
document.location = url;
|
document.location = url;
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Could not start build'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp['message'] || 'The build could not be started',
|
|
||||||
"title": "Could not start build",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteTrigger = function(trigger) {
|
$scope.deleteTrigger = function(trigger) {
|
||||||
|
@ -1750,18 +1685,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
|
|
||||||
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Could not revoke authorization'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.message || 'Could not revoke authorization',
|
|
||||||
"title": "Cannot revoke authorization",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadLogs = function() {
|
$scope.loadLogs = function() {
|
||||||
|
@ -1770,7 +1694,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadInvoices = function() {
|
$scope.loadInvoices = function() {
|
||||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
|
||||||
$scope.invoicesShown++;
|
$scope.invoicesShown++;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1956,9 +1879,6 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, I
|
||||||
|
|
||||||
// Fetch the image's changes.
|
// Fetch the image's changes.
|
||||||
fetchChanges();
|
fetchChanges();
|
||||||
|
|
||||||
$('#copyClipboard').clipboardCopy();
|
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -2226,13 +2146,14 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
||||||
'teamname': teamname
|
'teamname': teamname
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot delete team', function() {
|
||||||
|
$scope.currentDeleteTeam = null;
|
||||||
|
});
|
||||||
|
|
||||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||||
delete $scope.organization.teams[teamname];
|
delete $scope.organization.teams[teamname];
|
||||||
$scope.currentDeleteTeam = null;
|
$scope.currentDeleteTeam = null;
|
||||||
}, function() {
|
}, errorHandler);
|
||||||
$('#cannotchangeModal').modal({});
|
|
||||||
$scope.currentDeleteTeam = null;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var loadOrganization = function() {
|
var loadOrganization = function() {
|
||||||
|
@ -2575,9 +2496,9 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
||||||
}, function(result) {
|
}, function(resp) {
|
||||||
$scope.creating = false;
|
$scope.creating = false;
|
||||||
$scope.createError = result.data.error_description || result.data;
|
$scope.createError = ApiService.getErrorMessage(resp);
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
$('#orgName').popover('show');
|
$('#orgName').popover('show');
|
||||||
});
|
});
|
||||||
|
@ -2654,18 +2575,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
$location.path('/organization/' + orgname + '/admin');
|
$location.path('/organization/' + orgname + '/admin');
|
||||||
}, 500);
|
}, 500);
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Could not delete application'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.message || 'Could not delete application',
|
|
||||||
"title": "Cannot delete application",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateApplication = function() {
|
$scope.updateApplication = function() {
|
||||||
|
@ -2683,22 +2593,13 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
||||||
delete $scope.application['gravatar_email'];
|
delete $scope.application['gravatar_email'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
|
||||||
|
$scope.updating = false;
|
||||||
|
});
|
||||||
|
|
||||||
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
|
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
|
||||||
$scope.application = resp;
|
$scope.application = resp;
|
||||||
$scope.updating = false;
|
}, errorHandler);
|
||||||
}, function(resp) {
|
|
||||||
$scope.updating = false;
|
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.message || 'Could not update application',
|
|
||||||
"title": "Cannot update application",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.resetClientSecret = function() {
|
$scope.resetClientSecret = function() {
|
||||||
|
@ -2711,18 +2612,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
||||||
|
|
||||||
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
|
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
|
||||||
$scope.application = resp;
|
$scope.application = resp;
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Could not reset client secret'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.message || 'Could not reset client secret',
|
|
||||||
"title": "Cannot reset client secret",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var loadOrganization = function() {
|
var loadOrganization = function() {
|
||||||
|
@ -2818,18 +2708,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
|
|
||||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||||
$scope.loadUsersInternal();
|
$scope.loadUsersInternal();
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Could not change user'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data.message : 'Could not change user',
|
|
||||||
"title": "Cannot change user",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteUser = function(user) {
|
$scope.deleteUser = function(user) {
|
||||||
|
@ -2841,49 +2720,10 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
|
|
||||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||||
$scope.loadUsersInternal();
|
$scope.loadUsersInternal();
|
||||||
}, function(resp) {
|
}, ApiService.errorDisplay('Cannot delete user'));
|
||||||
bootbox.dialog({
|
|
||||||
"message": resp.data ? resp.data.message : 'Could not delete user',
|
|
||||||
"title": "Cannot delete user",
|
|
||||||
"buttons": {
|
|
||||||
"close": {
|
|
||||||
"label": "Close",
|
|
||||||
"className": "btn-primary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var seatUsageLoaded = function(usage) {
|
$scope.loadUsers();
|
||||||
$scope.usageLoading = false;
|
|
||||||
|
|
||||||
if (usage.count > usage.allowed) {
|
|
||||||
$scope.limit = 'over';
|
|
||||||
} else if (usage.count == usage.allowed) {
|
|
||||||
$scope.limit = 'at';
|
|
||||||
} else if (usage.count >= usage.allowed * 0.7) {
|
|
||||||
$scope.limit = 'near';
|
|
||||||
} else {
|
|
||||||
$scope.limit = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$scope.chart) {
|
|
||||||
$scope.chart = new UsageChart();
|
|
||||||
$scope.chart.draw('seat-usage-chart');
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.chart.update(usage.count, usage.allowed);
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadSeatUsage = function() {
|
|
||||||
$scope.usageLoading = true;
|
|
||||||
ApiService.getSeatCount().then(function(resp) {
|
|
||||||
seatUsageLoaded(resp);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSeatUsage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TourCtrl($scope, $location) {
|
function TourCtrl($scope, $location) {
|
||||||
|
|
|
@ -148,6 +148,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
||||||
var ch = dimensions.ch;
|
var ch = dimensions.ch;
|
||||||
|
|
||||||
// Set the height of the container so that it never goes offscreen.
|
// Set the height of the container so that it never goes offscreen.
|
||||||
|
if (!$('#' + container).removeOverscroll) { return; }
|
||||||
|
|
||||||
$('#' + container).removeOverscroll();
|
$('#' + container).removeOverscroll();
|
||||||
var viewportHeight = $(window).height();
|
var viewportHeight = $(window).height();
|
||||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||||
|
|
7
static/lib/ZeroClipboard.min.js
vendored
Executable file → Normal file
7
static/lib/ZeroClipboard.min.js
vendored
Executable file → Normal file
File diff suppressed because one or more lines are too long
BIN
static/lib/ZeroClipboard.swf
Executable file → Normal file
BIN
static/lib/ZeroClipboard.swf
Executable file → Normal file
Binary file not shown.
|
@ -53,7 +53,7 @@
|
||||||
<label for="orgName">Organization Name</label>
|
<label for="orgName">Organization Name</label>
|
||||||
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
||||||
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
||||||
data-placement="right" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
|
data-placement="bottom" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||||
<span class="description">This will also be the namespace for your repositories</span>
|
<span class="description">This will also be the namespace for your repositories</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,13 @@
|
||||||
</span>
|
</span>
|
||||||
<i class="fa fa-upload visible-lg"></i>
|
<i class="fa fa-upload visible-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
data-title="Administrators can view and download the full invoice history for their organization">
|
||||||
|
Invoice History
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-calendar visible-lg"></i>
|
||||||
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||||
|
@ -48,13 +55,6 @@
|
||||||
</span>
|
</span>
|
||||||
<i class="fa fa-bar-chart-o visible-lg"></i>
|
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
|
||||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
|
||||||
data-title="Administrators can view and download the full invoice history for their organization">
|
|
||||||
Invoice History
|
|
||||||
</span>
|
|
||||||
<i class="fa fa-calendar visible-lg"></i>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
data-title="All plans have a free trial">
|
data-title="All plans have a free trial">
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<div class="feature present"></div>
|
<div class="feature present"></div>
|
||||||
<div class="feature present"></div>
|
<div class="feature present"></div>
|
||||||
<div class="feature present"></div>
|
<div class="feature present"></div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
<div class="feature present"></div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||||
<div class="feature present"></div>
|
<div class="feature present"></div>
|
||||||
|
@ -93,9 +93,9 @@
|
||||||
<div class="feature present">SSL Encryption</div>
|
<div class="feature present">SSL Encryption</div>
|
||||||
<div class="feature present">Robot accounts</div>
|
<div class="feature present">Robot accounts</div>
|
||||||
<div class="feature present">Dockerfile Build</div>
|
<div class="feature present">Dockerfile Build</div>
|
||||||
|
<div class="feature present">Invoice History</div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
|
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
|
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
|
||||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
|
|
||||||
<div class="feature present">Free Trial</div>
|
<div class="feature present">Free Trial</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()">Build Triggers</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()"
|
||||||
|
quay-require="['BUILD_SUPPORT']">Build Triggers</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||||
|
@ -225,7 +226,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Triggers tab -->
|
<!-- Triggers tab -->
|
||||||
<div id="trigger" class="tab-pane">
|
<div id="trigger" class="tab-pane" quay-require="['BUILD_SUPPORT']">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Build Triggers
|
<div class="panel-heading">Build Triggers
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
|
||||||
|
@ -377,24 +378,6 @@
|
||||||
counter="showNewNotificationCounter"
|
counter="showNewNotificationCounter"
|
||||||
notification-created="handleNotificationCreated(notification)"></div>
|
notification-created="handleNotificationCreated(notification)"></div>
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="cannotchangeModal">
|
|
||||||
<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">Cannot change</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
The selected action could not be performed because you do not have that authority.
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="makepublicModal">
|
<div class="modal fade" id="makepublicModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
@ -441,26 +424,6 @@
|
||||||
</div><!-- /.modal -->
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="channgechangepermModal">
|
|
||||||
<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">Cannot change permissions</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
|
|
||||||
<span ng-show="changePermError">{{ changePermError }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="confirmdeleteModal">
|
<div class="modal fade" id="confirmdeleteModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|
|
@ -8,9 +8,6 @@
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active">
|
<li class="active">
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -19,19 +16,8 @@
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<!-- License tab -->
|
|
||||||
<div id="license" class="tab-pane active">
|
|
||||||
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
|
|
||||||
<!-- Chart -->
|
|
||||||
<div>
|
|
||||||
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
|
|
||||||
<span class="usage-caption" ng-show="chart">Seat Usage</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Users tab -->
|
<!-- Users tab -->
|
||||||
<div id="users" class="tab-pane">
|
<div id="users" class="tab-pane active">
|
||||||
<div class="quay-spinner" ng-show="!users"></div>
|
<div class="quay-spinner" ng-show="!users"></div>
|
||||||
<div class="alert alert-error" ng-show="usersError">
|
<div class="alert alert-error" ng-show="usersError">
|
||||||
{{ usersError }}
|
{{ usersError }}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-show="hasPaidBusinessPlan" quay-require="['BILLING']">
|
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||||
bs-tooltip=""
|
bs-tooltip=""
|
||||||
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
|
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
|
||||||
ng-show="repo.can_write || buildHistory.length">
|
quay-show="Features.BUILD_SUPPORT && (repo.can_write || buildHistory.length)">
|
||||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
<i class="fa fa-tasks fa-lg"></i>
|
<i class="fa fa-tasks fa-lg"></i>
|
||||||
<span class="count" ng-class="runningBuilds.length ? 'visible' : ''"><span>{{ runningBuilds.length ? runningBuilds.length : '' }}</span></span>
|
<span class="count" ng-class="runningBuilds.length ? 'visible' : ''"><span>{{ runningBuilds.length ? runningBuilds.length : '' }}</span></span>
|
||||||
|
@ -58,16 +58,9 @@
|
||||||
<span class="pull-command visible-md-inline">
|
<span class="pull-command visible-md-inline">
|
||||||
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
|
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
|
<div class="copy-box" hovering-message="true" value="'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name"></div>
|
||||||
<span id="copyClipboard" class="input-group-addon" data-title="Copy to Clipboard" data-clipboard-target="pull-text">
|
|
||||||
<i class="fa fa-copy"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
|
||||||
Copied to clipboard
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,23 +35,18 @@
|
||||||
</div><!-- /.modal-dialog -->
|
</div><!-- /.modal-dialog -->
|
||||||
</div><!-- /.modal -->
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
{% if not has_billing %}
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="overlicenseModal" data-backdrop="static">
|
<div class="modal fade" id="cannotContactService" data-backdrop="static">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Cannot create user</h4>
|
<h4 class="modal-title">Cannot Contact External Service</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
A new user cannot be created as this organization has reached its licensed seat count. Please contact your administrator.
|
A connection to an external service has failed. Please reload the page to try again.
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<a href="javascript:void(0)" class="btn btn-primary" data-dismiss="modal" onclick="location = '/signin'">Sign In</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
</div><!-- /.modal-dialog -->
|
</div><!-- /.modal-dialog -->
|
||||||
</div><!-- /.modal -->
|
</div><!-- /.modal -->
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Binary file not shown.
|
@ -196,7 +196,7 @@ def build_index_specs():
|
||||||
IndexTestSpec(url_for('index.put_repository_auth', repository=PUBLIC_REPO),
|
IndexTestSpec(url_for('index.put_repository_auth', repository=PUBLIC_REPO),
|
||||||
NO_REPO, 501, 501, 501, 501).set_method('PUT'),
|
NO_REPO, 501, 501, 501, 501).set_method('PUT'),
|
||||||
|
|
||||||
IndexTestSpec(url_for('index.get_search'), NO_REPO, 501, 501, 501, 501),
|
IndexTestSpec(url_for('index.get_search'), NO_REPO, 200, 200, 200, 200),
|
||||||
|
|
||||||
IndexTestSpec(url_for('index.ping'), NO_REPO, 200, 200, 200, 200),
|
IndexTestSpec(url_for('index.ping'), NO_REPO, 200, 200, 200, 200),
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ from endpoints.api.search import FindRepositories, EntitySearch
|
||||||
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||||
RepositoryBuildList)
|
RepositoryBuildList)
|
||||||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
|
||||||
|
RegenerateOrgRobot, RegenerateUserRobot)
|
||||||
|
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
|
@ -37,7 +39,7 @@ from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repos
|
||||||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||||
|
|
||||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1632,6 +1634,19 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(OrgRobot, orgname="buynlarge", robot_shortname="Z7PD")
|
self._set_url(OrgRobot, orgname="buynlarge", robot_shortname="Z7PD")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
def test_put_anonymous(self):
|
def test_put_anonymous(self):
|
||||||
self._run_test('PUT', 401, None, None)
|
self._run_test('PUT', 401, None, None)
|
||||||
|
|
||||||
|
@ -1644,6 +1659,7 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
||||||
def test_put_devtable(self):
|
def test_put_devtable(self):
|
||||||
self._run_test('PUT', 400, 'devtable', None)
|
self._run_test('PUT', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 401, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
@ -3040,6 +3056,19 @@ class TestUserRobot5vdy(ApiTestCase):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(UserRobot, robot_shortname="robotname")
|
self._set_url(UserRobot, robot_shortname="robotname")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 400, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 400, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
def test_put_anonymous(self):
|
def test_put_anonymous(self):
|
||||||
self._run_test('PUT', 401, None, None)
|
self._run_test('PUT', 401, None, None)
|
||||||
|
|
||||||
|
@ -3052,6 +3081,7 @@ class TestUserRobot5vdy(ApiTestCase):
|
||||||
def test_put_devtable(self):
|
def test_put_devtable(self):
|
||||||
self._run_test('PUT', 201, 'devtable', None)
|
self._run_test('PUT', 201, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 401, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
@ -3065,6 +3095,42 @@ class TestUserRobot5vdy(ApiTestCase):
|
||||||
self._run_test('DELETE', 400, 'devtable', None)
|
self._run_test('DELETE', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegenerateUserRobot(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(RegenerateUserRobot, robot_shortname="robotname")
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 400, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 400, 'reader', None)
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegenerateOrgRobot(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(RegenerateOrgRobot, orgname="buynlarge", robot_shortname="robotname")
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestOrganizationBuynlarge(ApiTestCase):
|
class TestOrganizationBuynlarge(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -16,7 +16,8 @@ from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
||||||
from endpoints.api.search import FindRepositories, EntitySearch
|
from endpoints.api.search import FindRepositories, EntitySearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList
|
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList
|
||||||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
|
||||||
|
RegenerateUserRobot, RegenerateOrgRobot)
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
|
@ -40,7 +41,7 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember,
|
||||||
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
|
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
|
||||||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app.register_blueprint(api_bp, url_prefix='/api')
|
app.register_blueprint(api_bp, url_prefix='/api')
|
||||||
|
@ -1751,6 +1752,30 @@ class TestUserRobots(ApiTestCase):
|
||||||
robots = self.getRobotNames()
|
robots = self.getRobotNames()
|
||||||
assert not NO_ACCESS_USER + '+bender' in robots
|
assert not NO_ACCESS_USER + '+bender' in robots
|
||||||
|
|
||||||
|
def test_regenerate(self):
|
||||||
|
self.login(NO_ACCESS_USER)
|
||||||
|
|
||||||
|
# Create a robot.
|
||||||
|
json = self.putJsonResponse(UserRobot,
|
||||||
|
params=dict(robot_shortname='bender'),
|
||||||
|
expected_code=201)
|
||||||
|
|
||||||
|
token = json['token']
|
||||||
|
|
||||||
|
# Regenerate the robot.
|
||||||
|
json = self.postJsonResponse(RegenerateUserRobot,
|
||||||
|
params=dict(robot_shortname='bender'),
|
||||||
|
expected_code=200)
|
||||||
|
|
||||||
|
# Verify the token changed.
|
||||||
|
self.assertNotEquals(token, json['token'])
|
||||||
|
|
||||||
|
json2 = self.getJsonResponse(UserRobot,
|
||||||
|
params=dict(robot_shortname='bender'),
|
||||||
|
expected_code=200)
|
||||||
|
|
||||||
|
self.assertEquals(json['token'], json2['token'])
|
||||||
|
|
||||||
|
|
||||||
class TestOrgRobots(ApiTestCase):
|
class TestOrgRobots(ApiTestCase):
|
||||||
def getRobotNames(self):
|
def getRobotNames(self):
|
||||||
|
@ -1780,6 +1805,31 @@ class TestOrgRobots(ApiTestCase):
|
||||||
assert not ORGANIZATION + '+bender' in robots
|
assert not ORGANIZATION + '+bender' in robots
|
||||||
|
|
||||||
|
|
||||||
|
def test_regenerate(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Create a robot.
|
||||||
|
json = self.putJsonResponse(OrgRobot,
|
||||||
|
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||||
|
expected_code=201)
|
||||||
|
|
||||||
|
token = json['token']
|
||||||
|
|
||||||
|
# Regenerate the robot.
|
||||||
|
json = self.postJsonResponse(RegenerateOrgRobot,
|
||||||
|
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||||
|
expected_code=200)
|
||||||
|
|
||||||
|
# Verify the token changed.
|
||||||
|
self.assertNotEquals(token, json['token'])
|
||||||
|
|
||||||
|
json2 = self.getJsonResponse(OrgRobot,
|
||||||
|
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||||
|
expected_code=200)
|
||||||
|
|
||||||
|
self.assertEquals(json['token'], json2['token'])
|
||||||
|
|
||||||
|
|
||||||
class TestLogs(ApiTestCase):
|
class TestLogs(ApiTestCase):
|
||||||
def test_user_logs(self):
|
def test_user_logs(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import argparse
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
def encrypt(message, output_filename):
|
|
||||||
private_key_file = 'conf/stack/license_key'
|
|
||||||
with open(private_key_file, 'r') as private_key:
|
|
||||||
encryptor = RSA.importKey(private_key)
|
|
||||||
|
|
||||||
encrypted_data = encryptor.decrypt(message)
|
|
||||||
|
|
||||||
with open(output_filename, 'wb') as encrypted_file:
|
|
||||||
encrypted_file.write(encrypted_data)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Create a license file.')
|
|
||||||
parser.add_argument('--users', type=int, default=20,
|
|
||||||
help='Number of users allowed by the license')
|
|
||||||
parser.add_argument('--days', type=int, default=30,
|
|
||||||
help='Number of days for which the license is valid')
|
|
||||||
parser.add_argument('--warn', type=int, default=7,
|
|
||||||
help='Number of days prior to expiration to warn users')
|
|
||||||
parser.add_argument('--output', type=str, required=True,
|
|
||||||
help='File in which to store the license')
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
args = parser.parse_args()
|
|
||||||
print ('Creating license for %s users for %s days in file: %s' %
|
|
||||||
(args.users, args.days, args.output))
|
|
||||||
|
|
||||||
license_data = {
|
|
||||||
'LICENSE_EXPIRATION': datetime.utcnow() + timedelta(days=args.days),
|
|
||||||
'LICENSE_USER_LIMIT': args.users,
|
|
||||||
'LICENSE_EXPIRATION_WARNING': datetime.utcnow() + timedelta(days=(args.days - args.warn)),
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt(pickle.dumps(license_data, 2), args.output)
|
|
|
@ -1,4 +1,4 @@
|
||||||
from app import stripe
|
import stripe
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
from util.invoice import renderInvoiceToHtml
|
from util.invoice import renderInvoiceToHtml
|
||||||
|
|
31
tools/phpmyadmin/Dockerfile
Normal file
31
tools/phpmyadmin/Dockerfile
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
FROM phusion/baseimage:0.9.9
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ENV HOME /root
|
||||||
|
ENV UPDATE_APT 2
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
# Install LAMP
|
||||||
|
RUN apt-get install -y lamp-server^
|
||||||
|
|
||||||
|
# Install phpMyAdmin
|
||||||
|
RUN mysqld & \
|
||||||
|
service apache2 start; \
|
||||||
|
sleep 5; \
|
||||||
|
printf y\\n\\n\\n1\\n | apt-get install -y phpmyadmin; \
|
||||||
|
sleep 15; \
|
||||||
|
mysqladmin -u root shutdown
|
||||||
|
|
||||||
|
|
||||||
|
# Setup phpmyadmin to run
|
||||||
|
RUN echo "Include /etc/phpmyadmin/apache.conf" >> /etc/apache2/apache2.conf
|
||||||
|
RUN rm /etc/phpmyadmin/config.inc.php
|
||||||
|
|
||||||
|
ADD config.inc.php /etc/phpmyadmin/config.inc.php
|
||||||
|
|
||||||
|
ADD run-admin.sh /etc/service/phpadmin/run
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["/sbin/my_init"]
|
59
tools/phpmyadmin/config.inc.php
Normal file
59
tools/phpmyadmin/config.inc.php
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Debian local configuration file
|
||||||
|
*
|
||||||
|
* This file overrides the settings made by phpMyAdmin interactive setup
|
||||||
|
* utility.
|
||||||
|
*
|
||||||
|
* For example configuration see
|
||||||
|
* /usr/share/doc/phpmyadmin/examples/config.sample.inc.php
|
||||||
|
* or
|
||||||
|
* /usr/share/doc/phpmyadmin/examples/config.manyhosts.inc.php
|
||||||
|
*
|
||||||
|
* NOTE: do not add security sensitive data to this file (like passwords)
|
||||||
|
* unless you really know what you're doing. If you do, any user that can
|
||||||
|
* run PHP or CGI on your webserver will be able to read them. If you still
|
||||||
|
* want to do this, make sure to properly secure the access to this file
|
||||||
|
* (also on the filesystem level).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load secret generated on postinst
|
||||||
|
include('/var/lib/phpmyadmin/blowfish_secret.inc.php');
|
||||||
|
|
||||||
|
// Load autoconf local config
|
||||||
|
include('/var/lib/phpmyadmin/config.inc.php');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server(s) configuration
|
||||||
|
*/
|
||||||
|
$i = 0;
|
||||||
|
// The $cfg['Servers'] array starts with $cfg['Servers'][1]. Do not use $cfg['Servers'][0].
|
||||||
|
// You can disable a server config entry by setting host to ''.
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read configuration from dbconfig-common
|
||||||
|
* You can regenerate it using: dpkg-reconfigure -plow phpmyadmin
|
||||||
|
*/
|
||||||
|
if (is_readable('/etc/phpmyadmin/config-db.php')) {
|
||||||
|
require('/etc/phpmyadmin/config-db.php');
|
||||||
|
} else {
|
||||||
|
error_log('phpmyadmin: Failed to load /etc/phpmyadmin/config-db.php.'
|
||||||
|
. ' Check group www-data has read access.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg['Servers'][$i]['auth_type'] = 'HTTP';
|
||||||
|
$cfg['Servers'][$i]['hide_db'] = '(mysql|information_schema|phpmyadmin)';
|
||||||
|
/* Server parameters */
|
||||||
|
$cfg['Servers'][$i]['host'] = 'db1.quay.io';
|
||||||
|
$cfg['Servers'][$i]['ssl'] = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* End of servers configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Directories for saving/loading files from server
|
||||||
|
*/
|
||||||
|
$cfg['UploadDir'] = '';
|
||||||
|
$cfg['SaveDir'] = '';
|
4
tools/phpmyadmin/run-admin.sh
Executable file
4
tools/phpmyadmin/run-admin.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
service apache2 start
|
||||||
|
mysqld
|
|
@ -1,4 +1,3 @@
|
||||||
from app import stripe
|
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
from util.useremails import send_confirmation_email
|
from util.useremails import send_confirmation_email
|
||||||
|
|
|
@ -30,7 +30,11 @@ class SendToMixpanel(Process):
|
||||||
while True:
|
while True:
|
||||||
mp_request = self._mp_queue.get()
|
mp_request = self._mp_queue.get()
|
||||||
logger.debug('Got queued mixpanel reqeust.')
|
logger.debug('Got queued mixpanel reqeust.')
|
||||||
|
try:
|
||||||
self._consumer.send(*json.loads(mp_request))
|
self._consumer.send(*json.loads(mp_request))
|
||||||
|
except:
|
||||||
|
# Make sure we don't crash if Mixpanel request fails.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FakeMixpanel(object):
|
class FakeMixpanel(object):
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import calendar
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from email.utils import formatdate
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from data import model
|
|
||||||
|
|
||||||
|
|
||||||
class ExpirationScheduler(object):
|
|
||||||
def __init__(self, utc_create_notifications_date, utc_terminate_processes_date):
|
|
||||||
self._scheduler = BackgroundScheduler()
|
|
||||||
self._termination_date = utc_terminate_processes_date
|
|
||||||
|
|
||||||
soon = datetime.now() + timedelta(seconds=1)
|
|
||||||
|
|
||||||
if utc_create_notifications_date > datetime.utcnow():
|
|
||||||
self._scheduler.add_job(model.delete_all_notifications_by_kind, 'date', run_date=soon,
|
|
||||||
args=['expiring_license'])
|
|
||||||
|
|
||||||
local_notifications_date = self._utc_to_local(utc_create_notifications_date)
|
|
||||||
self._scheduler.add_job(self._generate_notifications, 'date',
|
|
||||||
run_date=local_notifications_date)
|
|
||||||
else:
|
|
||||||
self._scheduler.add_job(self._generate_notifications, 'date', run_date=soon)
|
|
||||||
|
|
||||||
local_termination_date = self._utc_to_local(utc_terminate_processes_date)
|
|
||||||
self._scheduler.add_job(self._terminate, 'date', run_date=local_termination_date)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_date(date):
|
|
||||||
""" Output an RFC822 date format. """
|
|
||||||
if date is None:
|
|
||||||
return None
|
|
||||||
return formatdate(calendar.timegm(date.utctimetuple()))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _utc_to_local(utc_dt):
|
|
||||||
# get integer timestamp to avoid precision lost
|
|
||||||
timestamp = calendar.timegm(utc_dt.timetuple())
|
|
||||||
local_dt = datetime.fromtimestamp(timestamp)
|
|
||||||
return local_dt.replace(microsecond=utc_dt.microsecond)
|
|
||||||
|
|
||||||
def _generate_notifications(self):
|
|
||||||
for user in model.get_active_users():
|
|
||||||
model.create_unique_notification('expiring_license', user,
|
|
||||||
{'expires_at': self._format_date(self._termination_date)})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _terminate():
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self._scheduler.start()
|
|
|
@ -44,9 +44,9 @@ def matches_system_error(status_str):
|
||||||
KNOWN_MATCHES = ['lxc-start: invalid', 'lxc-start: failed to', 'lxc-start: Permission denied']
|
KNOWN_MATCHES = ['lxc-start: invalid', 'lxc-start: failed to', 'lxc-start: Permission denied']
|
||||||
|
|
||||||
for match in KNOWN_MATCHES:
|
for match in KNOWN_MATCHES:
|
||||||
# 4 because we might have a Unix control code at the start.
|
# 10 because we might have a Unix control code at the start.
|
||||||
found = status_str.find(match[0:len(match) + 4])
|
found = status_str.find(match[0:len(match) + 10])
|
||||||
if found >= 0 and found <= 4:
|
if found >= 0 and found <= 10:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -477,6 +477,7 @@ class DockerfileBuildWorker(Worker):
|
||||||
container['Id'], container['Command'])
|
container['Id'], container['Command'])
|
||||||
docker_cl.kill(container['Id'])
|
docker_cl.kill(container['Id'])
|
||||||
self._timeout.set()
|
self._timeout.set()
|
||||||
|
|
||||||
except ConnectionError as exc:
|
except ConnectionError as exc:
|
||||||
raise WorkerUnhealthyException(exc.message)
|
raise WorkerUnhealthyException(exc.message)
|
||||||
|
|
||||||
|
@ -521,16 +522,6 @@ class DockerfileBuildWorker(Worker):
|
||||||
logger.info(filetype_msg)
|
logger.info(filetype_msg)
|
||||||
log_appender(filetype_msg)
|
log_appender(filetype_msg)
|
||||||
|
|
||||||
if c_type not in self._mime_processors:
|
|
||||||
log_appender('error', build_logs.PHASE)
|
|
||||||
repository_build.phase = 'error'
|
|
||||||
repository_build.save()
|
|
||||||
message = 'Unknown mime-type: %s' % c_type
|
|
||||||
log_appender(message, build_logs.ERROR)
|
|
||||||
raise JobException(message)
|
|
||||||
|
|
||||||
build_dir = self._mime_processors[c_type](docker_resource)
|
|
||||||
|
|
||||||
# Spawn a notification that the build has started.
|
# Spawn a notification that the build has started.
|
||||||
event_data = {
|
event_data = {
|
||||||
'build_id': repository_build.uuid,
|
'build_id': repository_build.uuid,
|
||||||
|
@ -544,6 +535,7 @@ class DockerfileBuildWorker(Worker):
|
||||||
subpage='build?current=%s' % repository_build.uuid,
|
subpage='build?current=%s' % repository_build.uuid,
|
||||||
pathargs=['build', repository_build.uuid])
|
pathargs=['build', repository_build.uuid])
|
||||||
|
|
||||||
|
|
||||||
# Setup a handler for spawning failure messages.
|
# Setup a handler for spawning failure messages.
|
||||||
def spawn_failure(message, event_data):
|
def spawn_failure(message, event_data):
|
||||||
event_data['error_message'] = message
|
event_data['error_message'] = message
|
||||||
|
@ -551,6 +543,29 @@ class DockerfileBuildWorker(Worker):
|
||||||
subpage='build?current=%s' % repository_build.uuid,
|
subpage='build?current=%s' % repository_build.uuid,
|
||||||
pathargs=['build', repository_build.uuid])
|
pathargs=['build', repository_build.uuid])
|
||||||
|
|
||||||
|
if c_type not in self._mime_processors:
|
||||||
|
log_appender('error', build_logs.PHASE)
|
||||||
|
repository_build.phase = 'error'
|
||||||
|
repository_build.save()
|
||||||
|
message = 'Unknown mime-type: %s' % c_type
|
||||||
|
log_appender(message, build_logs.ERROR)
|
||||||
|
spawn_failure(message, event_data)
|
||||||
|
raise JobException(message)
|
||||||
|
|
||||||
|
# Try to build the build directory package from the buildpack.
|
||||||
|
log_appender('unpacking', build_logs.PHASE)
|
||||||
|
repository_build.phase = 'unpacking'
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
|
build_dir = None
|
||||||
|
try:
|
||||||
|
build_dir = self._mime_processors[c_type](docker_resource)
|
||||||
|
except Exception as ex:
|
||||||
|
cur_message = ex.message or 'Error while unpacking build package'
|
||||||
|
log_appender(cur_message, build_logs.ERROR)
|
||||||
|
spawn_failure(cur_message, event_data)
|
||||||
|
raise JobException(cur_message)
|
||||||
|
|
||||||
# Start the build process.
|
# Start the build process.
|
||||||
try:
|
try:
|
||||||
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, access_token,
|
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, access_token,
|
||||||
|
@ -599,6 +614,7 @@ class DockerfileBuildWorker(Worker):
|
||||||
|
|
||||||
except WorkerUnhealthyException as exc:
|
except WorkerUnhealthyException as exc:
|
||||||
# Spawn a notification that the build has failed.
|
# Spawn a notification that the build has failed.
|
||||||
|
log_appender('Worker has become unhealthy. Will retry shortly.', build_logs.ERROR)
|
||||||
spawn_failure(exc.message, event_data)
|
spawn_failure(exc.message, event_data)
|
||||||
|
|
||||||
# Raise the exception to the queue.
|
# Raise the exception to the queue.
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
from app import webhook_queue
|
|
||||||
from workers.worker import Worker
|
|
||||||
|
|
||||||
|
|
||||||
root_logger = logging.getLogger('')
|
|
||||||
root_logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - %(funcName)s - %(message)s'
|
|
||||||
formatter = logging.Formatter(FORMAT)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookWorker(Worker):
|
|
||||||
def process_queue_item(self, job_details):
|
|
||||||
url = job_details['url']
|
|
||||||
payload = job_details['payload']
|
|
||||||
headers = {'Content-type': 'application/json'}
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
||||||
if resp.status_code/100 != 2:
|
|
||||||
logger.error('%s response for webhook to url: %s' % (resp.status_code,
|
|
||||||
url))
|
|
||||||
return False
|
|
||||||
except requests.exceptions.RequestException as ex:
|
|
||||||
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
|
||||||
|
|
||||||
worker = WebhookWorker(webhook_queue, poll_period_seconds=15,
|
|
||||||
reservation_seconds=3600)
|
|
||||||
worker.start()
|
|
|
@ -102,8 +102,8 @@ class Worker(object):
|
||||||
logger.debug('Running watchdog.')
|
logger.debug('Running watchdog.')
|
||||||
try:
|
try:
|
||||||
self.watchdog()
|
self.watchdog()
|
||||||
except WorkerUnhealthyException:
|
except WorkerUnhealthyException as exc:
|
||||||
logger.error('The worker has encountered an error and will not take new jobs.')
|
logger.error('The worker has encountered an error via watchdog and will not take new jobs: %s' % exc.message)
|
||||||
self.mark_current_incomplete(restore_retry=True)
|
self.mark_current_incomplete(restore_retry=True)
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|
||||||
|
@ -133,10 +133,10 @@ class Worker(object):
|
||||||
logger.warning('An error occurred processing request: %s', current_queue_item.body)
|
logger.warning('An error occurred processing request: %s', current_queue_item.body)
|
||||||
self.mark_current_incomplete(restore_retry=False)
|
self.mark_current_incomplete(restore_retry=False)
|
||||||
|
|
||||||
except WorkerUnhealthyException:
|
except WorkerUnhealthyException as exc:
|
||||||
logger.error('The worker has encountered an error and will not take new jobs. Job is being requeued.')
|
logger.error('The worker has encountered an error via the job and will not take new jobs: %s' % exc.message)
|
||||||
self._stop.set()
|
|
||||||
self.mark_current_incomplete(restore_retry=True)
|
self.mark_current_incomplete(restore_retry=True)
|
||||||
|
self._stop.set()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Close the db handle periodically
|
# Close the db handle periodically
|
||||||
|
|
Reference in a new issue