Merge master into branch
|
@ -1,11 +1,11 @@
|
|||
conf/stack
|
||||
screenshots
|
||||
tools
|
||||
test/data/registry
|
||||
venv
|
||||
.git
|
||||
.gitignore
|
||||
Bobfile
|
||||
README.md
|
||||
license.py
|
||||
requirements-nover.txt
|
||||
run-local.sh
|
||||
run-local.sh
|
|
@ -4,10 +4,10 @@ ENV DEBIAN_FRONTEND noninteractive
|
|||
ENV HOME /root
|
||||
|
||||
# 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
|
||||
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
|
||||
ADD requirements.txt requirements.txt
|
||||
|
|
|
@ -4,10 +4,10 @@ ENV DEBIAN_FRONTEND noninteractive
|
|||
ENV HOME /root
|
||||
|
||||
# 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
|
||||
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
|
||||
ADD requirements.txt requirements.txt
|
||||
|
@ -30,6 +30,7 @@ RUN cd grunt && npm install
|
|||
RUN cd grunt && grunt
|
||||
|
||||
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/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/notificationworker /etc/service/notificationworker
|
||||
|
||||
# TODO: Remove this after the prod CL push
|
||||
ADD conf/init/webhookworker /etc/service/webhookworker
|
||||
|
||||
# Download any external libs.
|
||||
RUN mkdir static/fonts static/ldn
|
||||
RUN venv/bin/python -m external_libraries
|
||||
|
|
52
app.py
|
@ -1,8 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
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.login import LoginManager
|
||||
from flask.ext.mail import Mail
|
||||
|
@ -21,11 +22,37 @@ from data.billing import Billing
|
|||
from data.buildlogs import BuildLogs
|
||||
from data.queue import WorkQueue
|
||||
from data.userevent import UserEventsBuilderModule
|
||||
from license import load_license
|
||||
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'
|
||||
LICENSE_FILENAME = 'conf/stack/license.enc'
|
||||
|
||||
|
@ -43,22 +70,17 @@ else:
|
|||
logger.debug('Loading default config.')
|
||||
app.config.from_object(DefaultConfig())
|
||||
|
||||
if os.path.exists(OVERRIDE_CONFIG_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME)
|
||||
app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME)
|
||||
if os.path.exists(OVERRIDE_CONFIG_PY_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_PY_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, '{}'))
|
||||
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('No license file found, please check your configuration')
|
||||
|
||||
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)
|
||||
|
||||
Principal(app, use_sessions=False)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
bind = 'unix:/tmp/gunicorn.sock'
|
||||
workers = 8
|
||||
workers = 16
|
||||
worker_class = 'gevent'
|
||||
timeout = 2000
|
||||
logconfig = 'conf/logging.conf'
|
||||
|
|
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;
|
||||
server_name _;
|
||||
|
||||
|
|
|
@ -163,6 +163,9 @@ class DefaultConfig(object):
|
|||
# Feature Flag: Whether to support GitHub build triggers.
|
||||
FEATURE_GITHUB_BUILD = False
|
||||
|
||||
# Feature Flag: Dockerfile build support.
|
||||
FEATURE_BUILD_SUPPORT = True
|
||||
|
||||
DISTRIBUTED_STORAGE_CONFIG = {
|
||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||
|
|
|
@ -41,6 +41,15 @@ PLANS = [
|
|||
'bus_features': False,
|
||||
'deprecated': True,
|
||||
},
|
||||
{
|
||||
'title': 'Yacht',
|
||||
'price': 5000,
|
||||
'privateRepos': 20,
|
||||
'stripeId': 'bus-small',
|
||||
'audience': 'For small businesses',
|
||||
'bus_features': True,
|
||||
'deprecated': True,
|
||||
},
|
||||
|
||||
# Active plans
|
||||
{
|
||||
|
@ -74,7 +83,7 @@ PLANS = [
|
|||
'title': 'Yacht',
|
||||
'price': 5000,
|
||||
'privateRepos': 20,
|
||||
'stripeId': 'bus-small',
|
||||
'stripeId': 'bus-coreos-trial',
|
||||
'audience': 'For small businesses',
|
||||
'bus_features': True,
|
||||
'deprecated': False,
|
||||
|
|
|
@ -17,6 +17,8 @@ SCHEME_DRIVERS = {
|
|||
'mysql': MySQLDatabase,
|
||||
'mysql+pymysql': MySQLDatabase,
|
||||
'sqlite': SqliteDatabase,
|
||||
'postgresql': PostgresqlDatabase,
|
||||
'postgresql+psycopg2': PostgresqlDatabase,
|
||||
}
|
||||
|
||||
db = Proxy()
|
||||
|
@ -32,7 +34,7 @@ def _db_from_url(url, db_kwargs):
|
|||
if parsed_url.username:
|
||||
db_kwargs['user'] = parsed_url.username
|
||||
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)
|
||||
|
||||
|
@ -74,6 +76,8 @@ class User(BaseModel):
|
|||
organization = BooleanField(default=False, index=True)
|
||||
robot = BooleanField(default=False, index=True)
|
||||
invoice_email = BooleanField(default=False)
|
||||
invalid_login_attempts = IntegerField(default=0)
|
||||
last_invalid_login = DateTimeField(default=datetime.utcnow)
|
||||
|
||||
|
||||
class TeamRole(BaseModel):
|
||||
|
|
|
@ -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():
|
||||
conn = op.get_bind()
|
||||
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')
|
||||
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')
|
||||
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():
|
||||
conn = op.get_bind()
|
||||
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')
|
||||
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')
|
||||
conn.execute('Insert Into webhook (public_id, repository_id, parameters) Select uuid, repository_id, config_json FROM repositorynotification Where event_id=%s And method_id=%s' % (event_id, method_id))
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
"""Add brute force prevention metadata to the user table.
|
||||
|
||||
Revision ID: 4fdb65816b8d
|
||||
Revises: 43e943c0639f
|
||||
Create Date: 2014-09-03 12:35:33.722435
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4fdb65816b8d'
|
||||
down_revision = '43e943c0639f'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('user', sa.Column('invalid_login_attempts', sa.Integer(), nullable=False, server_default="0"))
|
||||
op.add_column('user', sa.Column('last_invalid_login', sa.DateTime(), nullable=False, server_default=sa.func.now()))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('user', 'last_invalid_login')
|
||||
op.drop_column('user', 'invalid_login_attempts')
|
||||
### end Alembic commands ###
|
|
@ -203,7 +203,7 @@ def upgrade():
|
|||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_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(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
|
@ -375,7 +375,7 @@ def upgrade():
|
|||
sa.Column('command', sa.Text(), nullable=True),
|
||||
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||
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.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||
sa.ForeignKeyConstraint(['storage_id'], ['imagestorage.id'], ),
|
||||
|
|
36
data/migrations/versions/82297d834ad_add_us_west_location.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""add US West location
|
||||
|
||||
Revision ID: 82297d834ad
|
||||
Revises: 47670cbeced
|
||||
Create Date: 2014-08-15 13:35:23.834079
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '82297d834ad'
|
||||
down_revision = '47670cbeced'
|
||||
|
||||
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['imagestoragelocation'],
|
||||
[
|
||||
{'id':8, 'name':'s3_us_west_1'},
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
schema = gen_sqlalchemy_metadata(all_models)
|
||||
|
||||
op.execute(
|
||||
(imagestoragelocation.delete()
|
||||
.where(imagestoragelocation.c.name == op.inline_literal('s3_us_west_1')))
|
||||
|
||||
)
|
|
@ -35,6 +35,7 @@ def upgrade():
|
|||
{'id':4, 'name':'s3_ap_southeast_2'},
|
||||
{'id':5, 'name':'s3_ap_northeast_1'},
|
||||
{'id':6, 'name':'s3_sa_east_1'},
|
||||
{'id':7, 'name':'local'},
|
||||
])
|
||||
|
||||
op.create_table('imagestorageplacement',
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
"""Remove the old webhooks table.
|
||||
|
||||
Revision ID: f42b0ea7a4d
|
||||
Revises: 4fdb65816b8d
|
||||
Create Date: 2014-09-03 13:43:23.391464
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f42b0ea7a4d'
|
||||
down_revision = '4fdb65816b8d'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('webhook')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('webhook',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), nullable=False),
|
||||
sa.Column('public_id', mysql.VARCHAR(length=255), nullable=False),
|
||||
sa.Column('repository_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('parameters', mysql.LONGTEXT(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['repository_id'], [u'repository.id'], name=u'fk_webhook_repository_repository_id'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_default_charset=u'latin1',
|
||||
mysql_engine=u'InnoDB'
|
||||
)
|
||||
### end Alembic commands ###
|
|
@ -1,12 +1,17 @@
|
|||
import bcrypt
|
||||
import logging
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import json
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from data.database import *
|
||||
from util.validation import *
|
||||
from util.names import format_robot_username
|
||||
from util.backoff import exponential_backoff
|
||||
|
||||
|
||||
EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -68,10 +73,15 @@ class TooManyUsersException(DataModelException):
|
|||
pass
|
||||
|
||||
|
||||
def is_create_user_allowed():
|
||||
return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT']
|
||||
class TooManyLoginAttemptsException(Exception):
|
||||
def __init__(self, message, retry_after):
|
||||
super(TooManyLoginAttemptsException, self).__init__(message)
|
||||
self.retry_after = retry_after
|
||||
|
||||
|
||||
def is_create_user_allowed():
|
||||
return True
|
||||
|
||||
def create_user(username, password, email):
|
||||
""" Creates a regular user, if allowed. """
|
||||
if not validate_password(password):
|
||||
|
@ -181,6 +191,19 @@ def create_robot(robot_shortname, parent):
|
|||
except Exception as ex:
|
||||
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):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
|
@ -191,7 +214,6 @@ def lookup_robot(robot_username):
|
|||
|
||||
return found[0]
|
||||
|
||||
|
||||
def verify_robot(robot_username, password):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
found = list(joined.where(FederatedLogin.service_ident == password,
|
||||
|
@ -204,6 +226,25 @@ def verify_robot(robot_username, password):
|
|||
|
||||
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):
|
||||
try:
|
||||
|
@ -524,11 +565,30 @@ def verify_user(username_or_email, password):
|
|||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
if fetched.invalid_login_attempts > 0:
|
||||
can_retry_at = exponential_backoff(fetched.invalid_login_attempts, EXPONENTIAL_BACKOFF_SCALE,
|
||||
fetched.last_invalid_login)
|
||||
|
||||
if can_retry_at > now:
|
||||
retry_after = can_retry_at - now
|
||||
raise TooManyLoginAttemptsException('Too many login attempts.', retry_after.total_seconds())
|
||||
|
||||
if (fetched.password_hash and
|
||||
bcrypt.hashpw(password, fetched.password_hash) ==
|
||||
fetched.password_hash):
|
||||
|
||||
if fetched.invalid_login_attempts > 0:
|
||||
fetched.invalid_login_attempts = 0
|
||||
fetched.save()
|
||||
|
||||
return fetched
|
||||
|
||||
fetched.invalid_login_attempts += 1
|
||||
fetched.last_invalid_login = now
|
||||
fetched.save()
|
||||
|
||||
# We weren't able to authorize the user
|
||||
return None
|
||||
|
||||
|
@ -828,6 +888,34 @@ def get_all_repo_users(namespace_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):
|
||||
try:
|
||||
return (Repository
|
||||
|
@ -982,7 +1070,8 @@ def find_create_or_link_image(docker_image_id, repository, username, translation
|
|||
.join(Repository)
|
||||
.join(Visibility)
|
||||
.switch(Repository)
|
||||
.join(RepositoryPermission, JOIN_LEFT_OUTER))
|
||||
.join(RepositoryPermission, JOIN_LEFT_OUTER)
|
||||
.where(ImageStorage.uploading == False))
|
||||
|
||||
query = (_filter_to_repos_for_user(query, username)
|
||||
.where(Image.docker_image_id == docker_image_id))
|
||||
|
@ -1662,19 +1751,20 @@ def create_notification(kind_name, target, metadata={}):
|
|||
|
||||
def create_unique_notification(kind_name, target, metadata={}):
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
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()
|
||||
AdminTeam = Team.alias()
|
||||
AdminTeamMember = TeamMember.alias()
|
||||
|
@ -1712,6 +1802,11 @@ def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=F
|
|||
.switch(Notification)
|
||||
.where(Notification.uuid == id_filter))
|
||||
|
||||
if page:
|
||||
query = query.paginate(page, limit)
|
||||
elif limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import logging
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint, request, make_response, jsonify
|
||||
from flask import Blueprint, request, make_response, jsonify, session
|
||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||
from flask.ext.restful.utils.cors import crossdomain
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
@ -66,6 +67,11 @@ class Unauthorized(ApiException):
|
|||
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
|
||||
|
||||
|
||||
class FreshLoginRequired(ApiException):
|
||||
def __init__(self, payload=None):
|
||||
ApiException.__init__(self, 'fresh_login_required', 401, "Requires fresh login", payload)
|
||||
|
||||
|
||||
class ExceedsLicenseException(ApiException):
|
||||
def __init__(self, payload=None):
|
||||
ApiException.__init__(self, None, 402, 'Payment Required', payload)
|
||||
|
@ -87,6 +93,14 @@ def handle_api_error(error):
|
|||
return response
|
||||
|
||||
|
||||
@api_bp.app_errorhandler(model.TooManyLoginAttemptsException)
|
||||
@crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
|
||||
def handle_too_many_login_attempts(error):
|
||||
response = make_response('Too many login attempts', 429)
|
||||
response.headers['Retry-After'] = int(error.retry_after)
|
||||
return response
|
||||
|
||||
|
||||
def resource(*urls, **kwargs):
|
||||
def wrapper(api_resource):
|
||||
if not api_resource:
|
||||
|
@ -256,6 +270,26 @@ def require_user_permission(permission_class, scope=None):
|
|||
|
||||
require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER)
|
||||
require_user_admin = require_user_permission(UserAdminPermission, None)
|
||||
require_fresh_user_admin = require_user_permission(UserAdminPermission, None)
|
||||
|
||||
def require_fresh_login(func):
|
||||
@add_method_metadata('requires_fresh_login', True)
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
user = get_authenticated_user()
|
||||
if not user:
|
||||
raise Unauthorized()
|
||||
|
||||
logger.debug('Checking fresh login for user %s', user.username)
|
||||
|
||||
last_login = session.get('login_time', datetime.datetime.now() - datetime.timedelta(minutes=60))
|
||||
valid_span = datetime.datetime.now() - datetime.timedelta(minutes=10)
|
||||
|
||||
if last_login >= valid_span:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
raise FreshLoginRequired()
|
||||
return wrapped
|
||||
|
||||
|
||||
def require_scope(scope_object):
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask import request
|
|||
from app import billing
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||
require_user_admin, show_if, hide_if)
|
||||
require_user_admin, show_if, hide_if, abort)
|
||||
from endpoints.api.subscribe import subscribe, subscription_view
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
@ -23,7 +23,11 @@ def get_card(user):
|
|||
}
|
||||
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus and cus.default_card:
|
||||
# Find the default card.
|
||||
default_card = None
|
||||
|
@ -46,7 +50,11 @@ def get_card(user):
|
|||
|
||||
def set_card(user, token):
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus:
|
||||
try:
|
||||
cus.card = token
|
||||
|
@ -55,6 +63,8 @@ def set_card(user, token):
|
|||
return carderror_response(exc)
|
||||
except stripe.InvalidRequestError as exc:
|
||||
return carderror_response(exc)
|
||||
except stripe.APIConnectionError as e:
|
||||
return carderror_response(e)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||
try:
|
||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
return {
|
||||
'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)
|
||||
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
@ -291,7 +308,10 @@ class OrganizationPlan(ApiResource):
|
|||
private_repos = model.get_private_repo_count(orgname)
|
||||
organization = model.get_organization(orgname)
|
||||
if organization.stripe_id:
|
||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
|
|
@ -119,6 +119,11 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||
if internal is not None:
|
||||
new_operation['internal'] = True
|
||||
|
||||
if include_internal:
|
||||
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
|
||||
if requires_fresh_login is not None:
|
||||
new_operation['requires_fresh_login'] = True
|
||||
|
||||
if not internal or (internal and include_internal):
|
||||
operations.append(new_operation)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ def image_view(image):
|
|||
'dbid': image.id,
|
||||
'size': extended_props.image_size,
|
||||
'locations': list(image.storage.locations),
|
||||
'uploading': image.storage.uploading,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -35,6 +35,14 @@ class UserRobotList(ApiResource):
|
|||
@internal_only
|
||||
class UserRobot(ApiResource):
|
||||
""" 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
|
||||
@nickname('createUserRobot')
|
||||
def put(self, robot_shortname):
|
||||
|
@ -79,6 +87,18 @@ class OrgRobotList(ApiResource):
|
|||
@related_user_resource(UserRobot)
|
||||
class OrgRobot(ApiResource):
|
||||
""" 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)
|
||||
@nickname('createOrgRobot')
|
||||
def put(self, orgname, robot_shortname):
|
||||
|
@ -103,3 +123,38 @@ class OrgRobot(ApiResource):
|
|||
return 'Deleted', 204
|
||||
|
||||
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):
|
||||
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):
|
||||
view = {
|
||||
|
@ -74,19 +77,29 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
response_json = subscription_view(cus.subscription, private_repos)
|
||||
status_code = 201
|
||||
|
||||
else:
|
||||
# Change the plan
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
if plan_found['price'] == 0:
|
||||
if cus.subscription is not None:
|
||||
# We only have to cancel the subscription if they actually have one
|
||||
cus.cancel_subscription()
|
||||
cus.save()
|
||||
try:
|
||||
cus.cancel_subscription()
|
||||
cus.save()
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
|
||||
check_repository_usage(user, plan_found)
|
||||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
|
||||
|
@ -101,6 +114,8 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
cus.save()
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
response_json = subscription_view(cus.subscription, private_repos)
|
||||
check_repository_usage(user, plan_found)
|
||||
|
|
|
@ -42,24 +42,6 @@ class SuperUserLogs(ApiResource):
|
|||
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):
|
||||
return {
|
||||
'username': user.username,
|
||||
|
|
|
@ -350,8 +350,8 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
(robot_namespace, shortname) = parse_robot_username(user.username)
|
||||
return AdministerOrganizationPermission(robot_namespace).can()
|
||||
|
||||
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
|
||||
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
|
||||
repo_users = list(model.get_all_repo_users_transitive(base_namespace, base_repository))
|
||||
read_robots = [robot_view(user) for user in repo_users if is_valid_robot(user)]
|
||||
|
||||
return {
|
||||
'namespace': base_namespace,
|
||||
|
|
|
@ -7,8 +7,9 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
|
|||
|
||||
from app import app, billing as stripe, authentication
|
||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||
log_action, internal_only, NotFound, require_user_admin,
|
||||
InvalidToken, require_scope, format_date, hide_if, show_if, license_error)
|
||||
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||
license_error, require_fresh_login)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from data import model
|
||||
|
@ -150,6 +151,7 @@ class User(ApiResource):
|
|||
return user_view(user)
|
||||
|
||||
@require_user_admin
|
||||
@require_fresh_login
|
||||
@nickname('changeUserDetails')
|
||||
@internal_only
|
||||
@validate_json_request('UpdateUser')
|
||||
|
@ -158,7 +160,7 @@ class User(ApiResource):
|
|||
user = get_authenticated_user()
|
||||
user_data = request.get_json()
|
||||
|
||||
try:
|
||||
try:
|
||||
if 'password' in user_data:
|
||||
logger.debug('Changing password for user: %s', user.username)
|
||||
log_action('account_change_password', user.username)
|
||||
|
@ -363,6 +365,37 @@ class Signin(ApiResource):
|
|||
return conduct_signin(username, password)
|
||||
|
||||
|
||||
@resource('/v1/signin/verify')
|
||||
@internal_only
|
||||
class VerifyUser(ApiResource):
|
||||
""" Operations for verifying the existing user. """
|
||||
schemas = {
|
||||
'VerifyUser': {
|
||||
'id': 'VerifyUser',
|
||||
'type': 'object',
|
||||
'description': 'Information required to verify the signed in user.',
|
||||
'required': [
|
||||
'password',
|
||||
],
|
||||
'properties': {
|
||||
'password': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s password',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('verifyUser')
|
||||
@validate_json_request('VerifyUser')
|
||||
def post(self):
|
||||
""" Verifies the signed in the user with the specified credentials. """
|
||||
signin_data = request.get_json()
|
||||
password = signin_data['password']
|
||||
return conduct_signin(get_authenticated_user().username, password)
|
||||
|
||||
|
||||
@resource('/v1/signout')
|
||||
@internal_only
|
||||
class Signout(ApiResource):
|
||||
|
@ -410,11 +443,24 @@ class Recovery(ApiResource):
|
|||
@internal_only
|
||||
class UserNotificationList(ApiResource):
|
||||
@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')
|
||||
def get(self):
|
||||
notifications = model.list_notifications(get_authenticated_user())
|
||||
def get(self, args):
|
||||
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 {
|
||||
'notifications': [notification_view(notification) for notification in notifications]
|
||||
'notifications': [notification_view(notification) for notification in notifications],
|
||||
'additional': has_more
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@ import logging
|
|||
import urlparse
|
||||
import json
|
||||
import string
|
||||
import datetime
|
||||
|
||||
from flask import make_response, render_template, request, abort
|
||||
from flask import make_response, render_template, request, abort, session
|
||||
from flask.ext.login import login_user, UserMixin
|
||||
from flask.ext.principal import identity_changed
|
||||
from random import SystemRandom
|
||||
|
@ -112,6 +113,7 @@ def common_login(db_user):
|
|||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username', {scopes.DIRECT_LOGIN})
|
||||
identity_changed.send(app, identity=new_identity)
|
||||
session['login_time'] = datetime.datetime.now()
|
||||
return True
|
||||
else:
|
||||
logger.debug('User could not be logged in, inactive?.')
|
||||
|
|
|
@ -413,8 +413,39 @@ def put_repository_auth(namespace, repository):
|
|||
|
||||
|
||||
@index.route('/search', methods=['GET'])
|
||||
@process_auth
|
||||
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')
|
||||
|
|
|
@ -184,6 +184,8 @@ class BuildFailureEvent(NotificationEvent):
|
|||
return 'build_failure'
|
||||
|
||||
def get_sample_data(self, repository):
|
||||
build_uuid = 'fake-build-id'
|
||||
|
||||
return build_event_data(repository, {
|
||||
'build_id': build_uuid,
|
||||
'build_name': 'some-fake-build',
|
||||
|
|
|
@ -37,6 +37,9 @@ class SocketReader(object):
|
|||
handler(buf)
|
||||
return buf
|
||||
|
||||
def tell(self):
|
||||
raise IOError('Stream is not seekable.')
|
||||
|
||||
|
||||
def image_is_uploading(repo_image):
|
||||
if repo_image is None:
|
||||
|
@ -367,6 +370,7 @@ def generate_ancestry(image_id, uuid, locations, parent_id=None, parent_uuid=Non
|
|||
if not parent_id:
|
||||
store.put_content(locations, store.image_ancestry_path(uuid), json.dumps([image_id]))
|
||||
return
|
||||
|
||||
data = store.get_content(parent_locations, store.image_ancestry_path(parent_uuid))
|
||||
data = json.loads(data)
|
||||
data.insert(0, image_id)
|
||||
|
@ -467,8 +471,13 @@ def put_image_json(namespace, repository, image_id):
|
|||
store.put_content(repo_image.storage.locations, json_path, request.data)
|
||||
|
||||
profile.debug('Generating image ancestry')
|
||||
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
||||
parent_locations)
|
||||
|
||||
try:
|
||||
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
||||
parent_locations)
|
||||
except IOError as ioe:
|
||||
profile.debug('Error when generating ancestry: %s' % ioe.message)
|
||||
abort(404)
|
||||
|
||||
profile.debug('Done')
|
||||
return make_response('true', 200)
|
||||
|
|
|
@ -60,7 +60,8 @@ module.exports = function(grunt) {
|
|||
removeEmptyAttributes: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true // For inline SVG
|
||||
}
|
||||
},
|
||||
quay: {
|
||||
|
|
|
@ -231,13 +231,15 @@ def initialize_database():
|
|||
LogEntryKind.create(name='delete_application')
|
||||
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='delete_repo_webhook')
|
||||
|
||||
LogEntryKind.create(name='add_repo_notification')
|
||||
LogEntryKind.create(name='delete_repo_notification')
|
||||
|
||||
LogEntryKind.create(name='regenerate_robot_token')
|
||||
|
||||
ImageStorageLocation.create(name='local_eu')
|
||||
ImageStorageLocation.create(name='local_us')
|
||||
|
||||
|
|
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
|
@ -32,5 +32,7 @@ raven
|
|||
python-ldap
|
||||
pycrypto
|
||||
logentries
|
||||
psycopg2
|
||||
pyyaml
|
||||
git+https://github.com/DevTable/aniso8601-fake.git
|
||||
git+https://github.com/DevTable/anunidecode.git
|
||||
|
|
|
@ -12,6 +12,7 @@ Pillow==2.5.1
|
|||
PyGithub==1.25.0
|
||||
PyMySQL==0.6.2
|
||||
PyPDF2==1.22
|
||||
PyYAML==3.11
|
||||
SQLAlchemy==0.9.7
|
||||
Werkzeug==0.9.6
|
||||
alembic==0.6.5
|
||||
|
@ -44,6 +45,7 @@ python-dateutil==2.2
|
|||
python-ldap==2.4.15
|
||||
python-magic==0.4.6
|
||||
pytz==2014.4
|
||||
psycopg2==2.5.3
|
||||
raven==5.0.0
|
||||
redis==2.10.1
|
||||
reportlab==2.7
|
||||
|
|
|
@ -9,33 +9,131 @@
|
|||
}
|
||||
}
|
||||
|
||||
.repo-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.repo-search {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#quay-logo {
|
||||
width: 80px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
#padding-container {
|
||||
padding: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.d3-tip {
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
}
|
||||
|
||||
#co-l-footer-wrapper {
|
||||
clear: both;
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin-bottom: -64px;
|
||||
}
|
||||
|
||||
#co-l-footer-wrapper #co-l-footer-push {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
#co-l-footer img {
|
||||
height: 50px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#co-l-footer .col-md-4 {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#co-l-footer {
|
||||
clear: both;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.co-img-bg-network {
|
||||
background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed;
|
||||
background-color: #2277ad;
|
||||
background-size: auto, 100% 100%;
|
||||
}
|
||||
|
||||
.co-m-navbar {
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.co-fx-box-shadow {
|
||||
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
-moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
-ms-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
-o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.co-fx-box-shadow-heavy {
|
||||
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid transparent;
|
||||
padding: 30px;
|
||||
|
||||
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-ms-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
-o-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
background-image: linear-gradient(to top, #535C66 0%,#6E8194 100%);
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-brand {
|
||||
padding-left: 14px;
|
||||
border-right: 1px solid rgb(134, 140, 163);
|
||||
padding-right: 14px;
|
||||
padding-top: 5px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a {
|
||||
color: white;
|
||||
letter-spacing: 0.5px;
|
||||
color: #428bca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a.active {
|
||||
color: #f04c5c;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
|
||||
color: #BEE1FF;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:hover, .navbar-default .navbar-nav>.open>a:focus {
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-view-element {
|
||||
|
@ -366,6 +464,22 @@ i.toggle-icon:hover {
|
|||
|
||||
.docker-auth-dialog .token-dialog-body .well {
|
||||
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 {
|
||||
|
@ -399,17 +513,6 @@ i.toggle-icon:hover {
|
|||
margin: 0 auto -176px;
|
||||
}
|
||||
|
||||
.footer-container, .push {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.footer-container.fixed {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.button-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -489,6 +592,14 @@ i.toggle-icon:hover {
|
|||
line-height: 25px;
|
||||
}
|
||||
|
||||
.logs-view-element .log .log-description code {
|
||||
max-width: 300px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs-view-element .log-performer {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -634,7 +745,7 @@ i.toggle-icon:hover {
|
|||
}
|
||||
|
||||
.user-notification.notification-animated {
|
||||
width: 21px;
|
||||
min-width: 21px;
|
||||
|
||||
transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
|
@ -671,12 +782,12 @@ i.toggle-icon:hover {
|
|||
.user-tools .user-tool {
|
||||
font-size: 24px;
|
||||
margin-top: 14px;
|
||||
color: white;
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
.user-tools i.user-tool:hover {
|
||||
cursor: pointer;
|
||||
color: #BEE1FF;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-box a {
|
||||
|
@ -728,7 +839,7 @@ i.toggle-icon:hover {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -1000,12 +1111,12 @@ i.toggle-icon:hover {
|
|||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small {
|
||||
.plans-list .plan.bus-small, .plans-list .plan.bus-coreos-trial {
|
||||
border-top: 6px solid #46ac39;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small .plan-box {
|
||||
.plans-list .plan.bus-small .plan-box, .plans-list .plan.bus-coreos-trial .plan-box {
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
|
@ -1040,6 +1151,10 @@ i.toggle-icon:hover {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.hidden-xs-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media (min-width: 991px) {
|
||||
.visible-md-inline {
|
||||
display: inline;
|
||||
|
@ -1056,6 +1171,13 @@ i.toggle-icon:hover {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hidden-xs-inline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.visible-xl {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1085,7 +1207,7 @@ i.toggle-icon:hover {
|
|||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small button {
|
||||
.plans-list .plan.bus-small button, .plans-list .plan.bus-coreos-trial button {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
@ -1230,14 +1352,64 @@ i.toggle-icon:hover {
|
|||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.plans .plan-faq dd{
|
||||
.plans .plan-faq dd {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.enterprise-plan .plan-combine {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.enterprise-plan .plan-combine .plus {
|
||||
font-size: 22px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.enterprise-plan a {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.enterprise-plan .plan-combine img {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.enterprise-tour .tour-section {
|
||||
margin-bottom: 50px !important;
|
||||
padding-bottom: 50px !important;
|
||||
border-bottom: 1px solid #eee !important;
|
||||
}
|
||||
|
||||
.enterprise-tour .tour-section p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.enterprise-tour .btn {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.feature-illustration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-illustration svg {
|
||||
margin: 0 auto;
|
||||
width: auto !important;
|
||||
max-width: 100%;
|
||||
max-height: 110px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.jumbotron .disclaimer-link {
|
||||
font-size: .3em;
|
||||
vertical-align: 23px;
|
||||
|
@ -1255,25 +1427,19 @@ i.toggle-icon:hover {
|
|||
color: #555;
|
||||
}
|
||||
|
||||
.landing-page .wrapper > nav {
|
||||
display: none;
|
||||
.landing-page #padding-container {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.landing-page .nav > li > a {
|
||||
border-radius: 4px;
|
||||
.landing-page .main-panel {
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.landing-page .nav > li > a:hover, .landing-page .nav > li > a:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.landing-page .nav .user-view {
|
||||
color: white !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.landing-page .user-tool {
|
||||
color: white;
|
||||
.landing-page.signedin .main-panel {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.landing {
|
||||
|
@ -1284,10 +1450,6 @@ i.toggle-icon:hover {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.landing-content {
|
||||
|
||||
}
|
||||
|
||||
.landing-background {
|
||||
z-index: 0;
|
||||
|
||||
|
@ -1297,10 +1459,13 @@ i.toggle-icon:hover {
|
|||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
background-color: #1d1d1d;
|
||||
background-image: url(../img/landing-back-opt.jpg);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background: url('/static/img/network-tile.png') left top repeat, linear-gradient(30deg, #2277ad, #144768) no-repeat left top fixed;
|
||||
background-color: #2277ad;
|
||||
background-size: auto, 100% 100%;
|
||||
}
|
||||
|
||||
.landing-page.signedin .landing-background {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.landing-filter {
|
||||
|
@ -1320,6 +1485,55 @@ i.toggle-icon:hover {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
.landing .call-to-action i.fa {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.landing .call-to-action {
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
|
||||
background: rgba(15, 131, 203, 0.6);
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.landing .announcement {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
display: block;
|
||||
background: rgba(8, 61, 95, 0.6);
|
||||
min-height: 45px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 45px;
|
||||
}
|
||||
|
||||
.landing .announcement .spacer {
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.landing .announcement img {
|
||||
height: 45px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.landing .announcement .plus {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.landing {
|
||||
color: white;
|
||||
|
||||
|
@ -1346,104 +1560,6 @@ i.toggle-icon:hover {
|
|||
font-size: 40px;
|
||||
}
|
||||
|
||||
.landing .header-bar .navbar-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.landing .header-bar form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.landing .header-bar {
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.landing .header-bar .user-tools a {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.landing .header-bar .user-tools i {
|
||||
margin-top: 0px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.landing .header-bar .user-tools i:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.landing .navbar-links a {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.landing .navbar-links li a:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.landing .nav .dropdown-menu a {
|
||||
color: black;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 971px) {
|
||||
.landing .navbar-collapse {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: block;
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
line-height: 28px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
border-top: 1px solid transparent;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.navbar-collapse.collapse {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
float: none!important;
|
||||
margin: 7.5px -15px;
|
||||
}
|
||||
|
||||
.navbar-nav>li {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.navbar-nav>li>a {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.navbar-collapse.collapse.in { display: block!important; }
|
||||
}
|
||||
|
||||
.landing .messages b {
|
||||
color: #59B2FF;
|
||||
}
|
||||
|
||||
.landing .messages h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
@ -1651,10 +1767,15 @@ form input.ng-valid.ng-dirty,
|
|||
font-size: 26px;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
padding: 10px;
|
||||
padding-bottom: 0px;
|
||||
border-top: 1px solid #eee;
|
||||
.footer-container, .push {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.footer-container.fixed {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.page-footer-padder {
|
||||
|
@ -1838,6 +1959,7 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.right-tag-controls {
|
||||
cursor: default;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
padding: 4px;
|
||||
|
@ -2151,6 +2273,14 @@ p.editable:hover i {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.copy-box-element.disabled .input-group-addon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.copy-box-element.disabled input {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.global-zeroclipboard-container embed {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -3141,6 +3271,10 @@ p.editable:hover i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-title {
|
||||
padding: 6px;
|
||||
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="input-group">
|
||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||
|
|
|
@ -10,19 +10,33 @@
|
|||
</div>
|
||||
<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="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>
|
||||
<i class="fa fa-refresh" ng-show="supportsRegenerate" ng-click="askRegenerate()"
|
||||
data-title="Regenerate Token"
|
||||
data-placement="left"
|
||||
bs-tooltip></i>
|
||||
</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()">
|
||||
<i class="fa fa-download"></i>
|
||||
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
|
||||
</span>
|
||||
<div id="clipboardCopied" style="display: none">
|
||||
Copied to clipboard
|
||||
<div class="clipboard-copied-message" style="display: none">
|
||||
Copied
|
||||
</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>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
≡
|
||||
</button>
|
||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||
<img src="/static/img/quay-box-white.png" data-title="Quay.io" data-placement="bottom" bs-tooltip>
|
||||
<img id="quay-logo" src="/static/img/black-horizontal.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Collapsable stuff -->
|
||||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||
<ul class="nav navbar-nav navbar-links">
|
||||
<li><a ng-href="/tour/" target="{{ appLinkTarget() }}">Tour</a></li>
|
||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
|
||||
<li><a ng-href="/tour/" target="{{ appLinkTarget() }}" quay-section="tour">Tour</a></li>
|
||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}" quay-section="repository">Repositories</a></li>
|
||||
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
|
||||
<li quay-require="['BILLING']"><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
|
||||
<li><a ng-href="{{ user.organizations.length ? '/organizations/' : '/tour/organizations/' }}" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}" quay-section="tutorial">Tutorial</a></li>
|
||||
<li quay-require="['BILLING']"><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-section="plans">Pricing</a></li>
|
||||
<li><a ng-href="{{ user.organizations.length ? '/organizations/' : '/tour/organizations/' }}" target="{{ appLinkTarget() }}" quay-section="organization">Organizations</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
@ -37,15 +37,7 @@
|
|||
<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" />
|
||||
{{ user.username }}
|
||||
<span class="badge user-notification notification-animated"
|
||||
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>
|
||||
<span class="notifications-bubble"></span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
@ -58,11 +50,7 @@
|
|||
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
||||
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
||||
Notifications
|
||||
<span class="badge user-notification"
|
||||
ng-class="notificationService.notificationClasses"
|
||||
ng-show="notificationService.notifications.length">
|
||||
{{ notificationService.notifications.length }}
|
||||
</span>
|
||||
<span class="notifications-bubble"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
|
|
|
@ -37,7 +37,8 @@
|
|||
<input id="log-filter" class="form-control" placeholder="Filter Logs" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Description</th>
|
||||
|
@ -77,5 +78,6 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<div class="aside-content">
|
||||
<div class="aside-header">
|
||||
<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 class="aside-body">
|
||||
<div ng-repeat="notification in notificationService.notifications">
|
||||
|
|
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 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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,16 +4,23 @@
|
|||
placeholder="Username or E-mail Address" ng-model="user.username" autofocus>
|
||||
<input type="password" class="form-control input-lg" name="password"
|
||||
placeholder="Password" ng-model="user.password">
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||
|
||||
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
|
||||
<i class="fa fa-circle"></i>
|
||||
<span class="inner-text">OR</span>
|
||||
</span>
|
||||
|
||||
<div class="external-login-button" provider="github" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
||||
<div class="external-login-button" provider="google" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
||||
</form>
|
||||
<div class="alert alert-warning" ng-show="tryAgainSoon > 0">
|
||||
Too many attempts have been made to login. Please try again in {{ tryAgainSoon }} second<span ng-if="tryAgainSoon != 1">s</span>.
|
||||
</div>
|
||||
|
||||
<span ng-show="tryAgainSoon == 0">
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||
|
||||
<span class="social-alternate" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">
|
||||
<i class="fa fa-circle"></i>
|
||||
<span class="inner-text">OR</span>
|
||||
</span>
|
||||
|
||||
<div class="external-login-button" provider="github" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
||||
<div class="external-login-button" provider="google" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
|
||||
<div class="alert alert-danger" ng-show="needsEmailVerification">
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
</span>
|
||||
<div class="external-login-button" provider="github"></div>
|
||||
<div class="external-login-button" provider="google"></div>
|
||||
<p class="help-block" quay-require="['BILLING']">No credit card required.</p>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="registering" style="text-align: center">
|
||||
|
|
|
@ -226,71 +226,208 @@
|
|||
|
||||
|
||||
<!-- Enterprise -->
|
||||
<div class="product-tour" ng-if="kind == 'enterprise'">
|
||||
<div class="product-tour enterprise-tour" ng-if="kind == 'enterprise'">
|
||||
<div class="tour-section row tour-header">
|
||||
<div class="col-md-12">
|
||||
<div class="tour-section-title">Quay.io in your data center</div>
|
||||
<div class="tour-section-title">Run Quay.io Behind Your Firewall</div>
|
||||
<div class="tour-section-description">
|
||||
All of the power of Quay.io, easily deployed to your data center via docker.
|
||||
</div>
|
||||
<div class="tour-section-description">
|
||||
<div class="row">
|
||||
<div class="alert alert-info col-md-4 col-md-offset-4"><strong>docker run quay.io/quay/enterprise</strong></div>
|
||||
<div class="col-lg-6 enterprise-plan col-lg-offset-3">
|
||||
<div class="plan-combine">
|
||||
<img src="/static/img/quay-logo.png">
|
||||
<span class="plus">+</span>
|
||||
<img src="/static/img/coreos.svg" style="height: 50px">
|
||||
</div>
|
||||
Quay.io has partnered with CoreOS to offer <a href="https://coreos.com/products/enterprise-registry">Enterprise Registry</a>, a version
|
||||
of Quay.io that can be hosted behind your firewall.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-sm-3 enterprise-icon">
|
||||
<span class="fa-stack fa-5x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-lock fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="tour-section-title">Take control of your own security</div>
|
||||
<div class="tour-section-description">
|
||||
The nature of machine images is that they often contain keys and passwords. We're pretty proud of the <a href="/security/">security</a> of our hosted offering, but we recognize that there are situations, such as compliance or auditing, where you must control your own end to end security. Quay.io Enterprise Edition runs in your own data centers, inside your firewall.
|
||||
<div class="tour-section row features">
|
||||
<div class="col-lg-4 col-md-4 col-sm-4 feature-desc">
|
||||
<div class="feature-illustration">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" width="100px" viewBox="0 0 144 144" enable-background="new 0 0 144 144" xml:space="preserve">
|
||||
<path fill="none" stroke="#010101" stroke-width="2" stroke-miterlimit="10" d="M124.979,62.559
|
||||
c0.695,3.413,1.058,6.944,1.058,10.56c0,29.104-23.591,52.695-52.695,52.695c-17.612,0-33.207-8.641-42.776-21.913"/>
|
||||
<path fill="none" stroke="#010101" stroke-width="2" stroke-miterlimit="10" d="M21.319,81.551c-0.44-2.744-0.671-5.561-0.671-8.431
|
||||
c0-29.104,23.591-52.695,52.695-52.695c17.442,0,32.906,8.474,42.497,21.532"/>
|
||||
<polygon fill="#010101" points="122.993,57.123 119.268,67.523 131.435,63.599 "/>
|
||||
<polygon fill="#010101" points="23.006,87.718 26.73,77.318 14.565,81.242 "/>
|
||||
<g enable-background="new ">
|
||||
<path d="M41.5,75.337v-5.679c0-0.204,0.071-0.403,0.215-0.598c0.143-0.194,0.307-0.302,0.491-0.322l4.757-0.737
|
||||
c0.225-0.716,0.552-1.493,0.982-2.332c-0.696-0.982-1.617-2.159-2.762-3.53c-0.144-0.225-0.215-0.43-0.215-0.613
|
||||
c0-0.246,0.071-0.45,0.215-0.614c0.45-0.613,1.289-1.523,2.517-2.731c1.228-1.207,2.036-1.811,2.425-1.811
|
||||
c0.225,0,0.439,0.072,0.645,0.215l3.529,2.762c0.696-0.368,1.483-0.695,2.363-0.981c0.225-2.21,0.46-3.785,0.706-4.727
|
||||
c0.143-0.491,0.45-0.737,0.921-0.737h5.708c0.225,0,0.43,0.077,0.614,0.23c0.185,0.153,0.287,0.333,0.307,0.537l0.706,4.696
|
||||
c0.695,0.204,1.462,0.521,2.302,0.951l3.622-2.731c0.164-0.143,0.368-0.215,0.614-0.215c0.225,0,0.44,0.082,0.645,0.245
|
||||
c2.946,2.722,4.42,4.358,4.42,4.911c0,0.184-0.072,0.379-0.215,0.583c-0.245,0.327-0.675,0.88-1.289,1.657
|
||||
c-0.614,0.778-1.074,1.392-1.382,1.842c0.471,0.982,0.818,1.821,1.044,2.517l4.665,0.706c0.205,0.041,0.379,0.148,0.521,0.322
|
||||
c0.144,0.174,0.215,0.373,0.215,0.599v5.678c0,0.205-0.071,0.404-0.215,0.599c-0.143,0.194-0.307,0.302-0.49,0.322l-4.758,0.736
|
||||
c-0.225,0.717-0.553,1.494-0.982,2.333c0.695,0.981,1.617,2.159,2.763,3.529c0.144,0.205,0.215,0.409,0.215,0.614
|
||||
c0,0.245-0.071,0.439-0.215,0.583c-0.471,0.613-1.315,1.529-2.532,2.747c-1.217,1.217-2.021,1.826-2.409,1.826
|
||||
c-0.225,0-0.44-0.072-0.645-0.215l-3.529-2.763c-0.757,0.389-1.545,0.706-2.363,0.951c-0.226,2.21-0.46,3.796-0.706,4.758
|
||||
c-0.144,0.491-0.45,0.736-0.921,0.736h-5.708c-0.226,0-0.43-0.077-0.614-0.23s-0.287-0.332-0.307-0.537l-0.706-4.695
|
||||
c-0.696-0.204-1.463-0.522-2.302-0.952l-3.622,2.732c-0.144,0.143-0.348,0.215-0.614,0.215c-0.226,0-0.44-0.082-0.645-0.246
|
||||
c-2.946-2.721-4.42-4.358-4.42-4.91c0-0.185,0.072-0.379,0.215-0.584c0.205-0.286,0.624-0.828,1.259-1.626
|
||||
c0.634-0.798,1.115-1.422,1.442-1.872c-0.471-0.9-0.829-1.739-1.074-2.518l-4.665-0.736c-0.205-0.02-0.379-0.117-0.522-0.291
|
||||
C41.571,75.762,41.5,75.562,41.5,75.337z M55.587,66.988c-1.534,1.535-2.302,3.387-2.302,5.556s0.768,4.021,2.302,5.555
|
||||
c1.535,1.535,3.386,2.302,5.556,2.302c2.168,0,4.021-0.767,5.555-2.302C68.233,76.564,69,74.713,69,72.544
|
||||
s-0.767-4.021-2.302-5.556c-1.535-1.534-3.387-2.302-5.555-2.302C58.974,64.687,57.122,65.454,55.587,66.988z M76.857,58.978
|
||||
v-4.297c0-0.327,1.523-0.645,4.572-0.951c0.266-0.593,0.573-1.125,0.922-1.596c-1.044-2.312-1.566-3.724-1.566-4.235
|
||||
c0-0.082,0.041-0.154,0.123-0.215c0.082-0.041,0.439-0.246,1.074-0.614s1.238-0.716,1.811-1.044c0.573-0.326,0.88-0.49,0.922-0.49
|
||||
c0.163,0,0.633,0.476,1.411,1.427c0.777,0.951,1.31,1.642,1.597,2.071c0.408-0.04,0.715-0.061,0.92-0.061s0.512,0.021,0.921,0.061
|
||||
c1.044-1.452,1.984-2.598,2.823-3.438l0.185-0.061c0.081,0,1.351,0.716,3.806,2.148c0.082,0.061,0.123,0.133,0.123,0.215
|
||||
c0,0.512-0.521,1.923-1.565,4.235c0.348,0.471,0.655,1.003,0.921,1.596c3.049,0.307,4.573,0.624,4.573,0.951v4.297
|
||||
c0,0.328-1.524,0.645-4.573,0.952c-0.246,0.552-0.553,1.084-0.921,1.596c1.044,2.312,1.565,3.725,1.565,4.235
|
||||
c0,0.082-0.041,0.153-0.123,0.215c-2.496,1.453-3.765,2.179-3.806,2.179c-0.164,0-0.634-0.48-1.411-1.442
|
||||
c-0.778-0.961-1.311-1.657-1.597-2.087c-0.409,0.041-0.716,0.062-0.921,0.062s-0.512-0.021-0.92-0.062
|
||||
c-0.287,0.43-0.819,1.126-1.597,2.087c-0.778,0.962-1.248,1.442-1.411,1.442c-0.042,0-1.311-0.726-3.807-2.179
|
||||
c-0.082-0.062-0.123-0.133-0.123-0.215c0-0.511,0.522-1.923,1.566-4.235c-0.369-0.512-0.676-1.044-0.922-1.596
|
||||
C78.381,59.622,76.857,59.306,76.857,58.978z M76.857,90.406v-4.297c0-0.327,1.523-0.645,4.572-0.951
|
||||
c0.266-0.594,0.573-1.125,0.922-1.596c-1.044-2.312-1.566-3.725-1.566-4.236c0-0.081,0.041-0.153,0.123-0.215
|
||||
c0.082-0.04,0.439-0.245,1.074-0.613s1.238-0.716,1.811-1.044c0.573-0.327,0.88-0.491,0.922-0.491c0.163,0,0.633,0.477,1.411,1.428
|
||||
c0.777,0.951,1.31,1.642,1.597,2.071c0.408-0.04,0.715-0.062,0.92-0.062s0.512,0.021,0.921,0.062
|
||||
c1.044-1.452,1.984-2.598,2.823-3.438l0.185-0.062c0.081,0,1.351,0.717,3.806,2.148c0.082,0.062,0.123,0.134,0.123,0.215
|
||||
c0,0.512-0.521,1.924-1.565,4.236c0.348,0.471,0.655,1.002,0.921,1.596c3.049,0.307,4.573,0.624,4.573,0.951v4.297
|
||||
c0,0.328-1.524,0.645-4.573,0.951c-0.246,0.553-0.553,1.085-0.921,1.597c1.044,2.312,1.565,3.724,1.565,4.235
|
||||
c0,0.081-0.041,0.153-0.123,0.215c-2.496,1.452-3.765,2.179-3.806,2.179c-0.164,0-0.634-0.48-1.411-1.442
|
||||
c-0.778-0.962-1.311-1.657-1.597-2.087c-0.409,0.041-0.716,0.062-0.921,0.062s-0.512-0.021-0.92-0.062
|
||||
c-0.287,0.43-0.819,1.125-1.597,2.087c-0.778,0.962-1.248,1.442-1.411,1.442c-0.042,0-1.311-0.727-3.807-2.179
|
||||
c-0.082-0.062-0.123-0.134-0.123-0.215c0-0.512,0.522-1.924,1.566-4.235c-0.369-0.512-0.676-1.044-0.922-1.597
|
||||
C78.381,91.051,76.857,90.734,76.857,90.406z M84.715,56.829c0,1.085,0.383,2.011,1.15,2.778c0.768,0.767,1.693,1.15,2.777,1.15
|
||||
s2.011-0.384,2.777-1.15c0.768-0.768,1.151-1.693,1.151-2.778c0-1.063-0.389-1.984-1.166-2.762
|
||||
c-0.778-0.777-1.698-1.167-2.763-1.167s-1.984,0.39-2.762,1.167C85.103,54.845,84.715,55.766,84.715,56.829z M84.715,88.258
|
||||
c0,1.085,0.383,2.011,1.15,2.777c0.768,0.768,1.693,1.151,2.777,1.151s2.011-0.384,2.777-1.151
|
||||
c0.768-0.767,1.151-1.692,1.151-2.777c0-1.063-0.389-1.984-1.166-2.762c-0.778-0.777-1.698-1.167-2.763-1.167
|
||||
s-1.984,0.39-2.762,1.167C85.103,86.273,84.715,87.194,84.715,88.258z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>Deployment Made Easy</h4>
|
||||
<p>Trigger container builds when your code is checked into Github and passes tests. Automatically pushed into your repository for immediate access by your servers.</p>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 col-sm-4 feature-desc">
|
||||
<div class="feature-illustration">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" width="140px" viewBox="0 0 202.5 144" enable-background="new 0 0 202.5 144" xml:space="preserve">
|
||||
<circle fill="none" stroke="#010101" stroke-width="1.9753" stroke-miterlimit="10" cx="72" cy="72" r="52.567"/>
|
||||
<circle fill="none" stroke="#010101" stroke-width="1.9753" stroke-miterlimit="10" cx="132.006" cy="72" r="52.567"/>
|
||||
<g>
|
||||
<path fill="#00A3A0" d="M90.699,77.453c0-0.552,0.019-1.091,0.055-1.617s0.109-1.094,0.219-1.703s0.247-1.175,0.414-1.696
|
||||
c0.167-0.521,0.391-1.028,0.672-1.523c0.281-0.495,0.604-0.917,0.969-1.266c0.364-0.349,0.81-0.627,1.336-0.836
|
||||
c0.526-0.208,1.106-0.312,1.742-0.312c0.094,0,0.312,0.112,0.656,0.336c0.344,0.224,0.731,0.474,1.164,0.75
|
||||
c0.433,0.276,0.995,0.526,1.688,0.75c0.692,0.224,1.388,0.336,2.086,0.336s1.394-0.112,2.086-0.336
|
||||
c0.692-0.224,1.255-0.474,1.688-0.75c0.433-0.276,0.82-0.526,1.164-0.75c0.344-0.224,0.562-0.336,0.656-0.336
|
||||
c0.636,0,1.216,0.104,1.742,0.312c0.526,0.208,0.972,0.487,1.336,0.836c0.364,0.349,0.688,0.771,0.969,1.266
|
||||
c0.281,0.495,0.505,1.003,0.672,1.523c0.167,0.521,0.305,1.086,0.414,1.696s0.183,1.177,0.219,1.703s0.055,1.065,0.055,1.617
|
||||
c0,1.25-0.38,2.236-1.141,2.961c-0.761,0.724-1.771,1.086-3.031,1.086H94.871c-1.261,0-2.271-0.362-3.031-1.086
|
||||
C91.079,79.689,90.699,78.703,90.699,77.453z M97.457,67.742c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242
|
||||
s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758
|
||||
S98.629,68.914,97.457,67.742z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#4BA0D8" d="M42.782,57.118c0-0.552,0.018-1.091,0.055-1.617c0.036-0.526,0.109-1.094,0.219-1.703
|
||||
s0.247-1.174,0.414-1.695c0.167-0.521,0.391-1.028,0.672-1.523c0.281-0.495,0.604-0.917,0.969-1.266
|
||||
c0.364-0.349,0.81-0.627,1.336-0.836c0.526-0.208,1.106-0.312,1.742-0.312c0.094,0,0.312,0.112,0.656,0.336
|
||||
c0.344,0.224,0.731,0.474,1.164,0.75c0.432,0.276,0.995,0.526,1.688,0.75c0.692,0.224,1.388,0.336,2.086,0.336
|
||||
c0.698,0,1.393-0.112,2.086-0.336c0.692-0.224,1.255-0.474,1.688-0.75c0.432-0.276,0.82-0.526,1.164-0.75
|
||||
c0.344-0.224,0.562-0.336,0.656-0.336c0.635,0,1.216,0.104,1.742,0.312c0.526,0.208,0.971,0.487,1.336,0.836
|
||||
c0.364,0.349,0.688,0.771,0.969,1.266c0.281,0.495,0.505,1.003,0.672,1.523c0.167,0.521,0.305,1.086,0.414,1.695
|
||||
s0.182,1.177,0.219,1.703c0.036,0.526,0.055,1.065,0.055,1.617c0,1.25-0.38,2.237-1.141,2.961
|
||||
c-0.761,0.724-1.771,1.086-3.031,1.086H46.954c-1.261,0-2.271-0.362-3.031-1.086C43.162,59.355,42.782,58.368,42.782,57.118z
|
||||
M49.54,47.407c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758
|
||||
s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758S50.712,48.579,49.54,47.407z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#4BA0D8" d="M27.422,90.324c0-0.552,0.018-1.091,0.055-1.617c0.036-0.526,0.109-1.094,0.219-1.703
|
||||
s0.247-1.175,0.414-1.695c0.167-0.521,0.391-1.028,0.672-1.523s0.604-0.917,0.969-1.266c0.364-0.349,0.81-0.628,1.336-0.836
|
||||
c0.526-0.208,1.106-0.312,1.742-0.312c0.094,0,0.312,0.112,0.656,0.336s0.731,0.474,1.164,0.75c0.432,0.276,0.995,0.526,1.688,0.75
|
||||
c0.692,0.224,1.388,0.336,2.086,0.336c0.698,0,1.393-0.112,2.086-0.336c0.692-0.224,1.255-0.474,1.688-0.75
|
||||
c0.432-0.276,0.82-0.526,1.164-0.75s0.562-0.336,0.656-0.336c0.635,0,1.216,0.104,1.742,0.312c0.526,0.208,0.971,0.487,1.336,0.836
|
||||
c0.364,0.349,0.688,0.771,0.969,1.266s0.505,1.003,0.672,1.523c0.167,0.521,0.305,1.086,0.414,1.695s0.182,1.177,0.219,1.703
|
||||
c0.036,0.526,0.055,1.065,0.055,1.617c0,1.25-0.38,2.236-1.141,2.961c-0.761,0.724-1.771,1.086-3.031,1.086H31.594
|
||||
c-1.261,0-2.271-0.362-3.031-1.086C27.802,92.561,27.422,91.574,27.422,90.324z M34.18,80.613
|
||||
c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758
|
||||
s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758S35.352,81.785,34.18,80.613z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#4BA0D8" d="M54.999,106.043c0-0.552,0.018-1.091,0.055-1.617c0.036-0.525,0.109-1.094,0.219-1.703
|
||||
s0.247-1.174,0.414-1.695c0.167-0.521,0.391-1.028,0.672-1.523c0.281-0.494,0.604-0.916,0.969-1.266
|
||||
c0.364-0.349,0.81-0.627,1.336-0.836c0.526-0.208,1.106-0.312,1.742-0.312c0.094,0,0.312,0.112,0.656,0.336
|
||||
c0.344,0.225,0.731,0.475,1.164,0.75c0.432,0.276,0.995,0.526,1.688,0.75c0.692,0.225,1.388,0.336,2.086,0.336
|
||||
c0.698,0,1.393-0.111,2.086-0.336c0.692-0.224,1.255-0.474,1.688-0.75c0.432-0.275,0.82-0.525,1.164-0.75
|
||||
c0.344-0.224,0.562-0.336,0.656-0.336c0.635,0,1.216,0.104,1.742,0.312c0.525,0.209,0.971,0.487,1.336,0.836
|
||||
c0.364,0.35,0.688,0.771,0.969,1.266c0.281,0.495,0.505,1.003,0.672,1.523c0.166,0.521,0.305,1.086,0.414,1.695
|
||||
s0.182,1.178,0.219,1.703c0.036,0.526,0.055,1.065,0.055,1.617c0,1.25-0.381,2.237-1.141,2.961
|
||||
c-0.761,0.724-1.771,1.086-3.031,1.086H59.171c-1.261,0-2.271-0.362-3.031-1.086C55.379,108.28,54.999,107.293,54.999,106.043z
|
||||
M61.757,96.332c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758
|
||||
s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758S62.929,97.504,61.757,96.332z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#00A651" d="M141.867,57.118c0-0.552,0.018-1.091,0.055-1.617c0.035-0.526,0.109-1.094,0.219-1.703
|
||||
s0.246-1.174,0.414-1.695c0.166-0.521,0.391-1.028,0.672-1.523c0.281-0.495,0.604-0.917,0.969-1.266
|
||||
c0.363-0.349,0.809-0.627,1.336-0.836c0.525-0.208,1.105-0.312,1.742-0.312c0.094,0,0.312,0.112,0.656,0.336
|
||||
c0.344,0.224,0.73,0.474,1.164,0.75c0.432,0.276,0.994,0.526,1.688,0.75c0.691,0.224,1.387,0.336,2.086,0.336
|
||||
c0.697,0,1.393-0.112,2.086-0.336c0.691-0.224,1.254-0.474,1.688-0.75c0.432-0.276,0.82-0.526,1.164-0.75
|
||||
c0.344-0.224,0.562-0.336,0.656-0.336c0.635,0,1.215,0.104,1.742,0.312c0.525,0.208,0.971,0.487,1.336,0.836
|
||||
c0.363,0.349,0.688,0.771,0.969,1.266c0.281,0.495,0.504,1.003,0.672,1.523c0.166,0.521,0.305,1.086,0.414,1.695
|
||||
s0.182,1.177,0.219,1.703c0.035,0.526,0.055,1.065,0.055,1.617c0,1.25-0.381,2.237-1.141,2.961
|
||||
c-0.762,0.724-1.771,1.086-3.031,1.086h-13.656c-1.262,0-2.271-0.362-3.031-1.086C142.246,59.354,141.867,58.368,141.867,57.118z
|
||||
M148.625,47.407c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758
|
||||
s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758S149.797,48.579,148.625,47.407z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#00A651" d="M132.005,102.458c0-0.552,0.019-1.091,0.055-1.617s0.109-1.094,0.219-1.703s0.247-1.175,0.414-1.695
|
||||
s0.391-1.028,0.672-1.523s0.604-0.917,0.969-1.266s0.81-0.628,1.336-0.836s1.106-0.312,1.742-0.312
|
||||
c0.094,0,0.312,0.112,0.656,0.336s0.731,0.474,1.164,0.75s0.995,0.526,1.688,0.75s1.388,0.336,2.086,0.336s1.394-0.112,2.086-0.336
|
||||
s1.255-0.474,1.688-0.75s0.82-0.526,1.164-0.75s0.562-0.336,0.656-0.336c0.636,0,1.216,0.104,1.742,0.312s0.972,0.487,1.336,0.836
|
||||
s0.688,0.771,0.969,1.266s0.505,1.003,0.672,1.523s0.305,1.086,0.414,1.695s0.183,1.177,0.219,1.703s0.055,1.065,0.055,1.617
|
||||
c0,1.25-0.38,2.236-1.141,2.961c-0.761,0.724-1.771,1.086-3.031,1.086h-13.656c-1.261,0-2.271-0.362-3.031-1.086
|
||||
C132.385,104.694,132.005,103.708,132.005,102.458z M138.763,92.747c-1.172-1.172-1.758-2.586-1.758-4.242s0.586-3.07,1.758-4.242
|
||||
s2.586-1.758,4.242-1.758s3.07,0.586,4.242,1.758s1.758,2.586,1.758,4.242s-0.586,3.07-1.758,4.242s-2.586,1.758-4.242,1.758
|
||||
S139.935,93.919,138.763,92.747z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>Teamwork Optimized</h4>
|
||||
<p>Control how the different teams and projects within your enterprise collaborate on repositories.</p>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 col-sm-4 feature-desc">
|
||||
<div class="feature-illustration">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" width="100px" viewBox="0 0 144 144" enable-background="new 0 0 144 144" xml:space="preserve">
|
||||
<path d="M103.5,123.357V110.5c0-0.595,0.208-1.101,0.625-1.518c0.416-0.416,0.922-0.625,1.518-0.625h0.715v-4.286
|
||||
c0-2.737,0.982-5.089,2.946-7.053c1.964-1.965,4.315-2.947,7.054-2.947c2.737,0,5.089,0.982,7.054,2.947
|
||||
c1.964,1.964,2.946,4.315,2.946,7.053v4.286h0.714c0.595,0,1.102,0.209,1.518,0.625c0.417,0.417,0.625,0.923,0.625,1.518v12.857
|
||||
c0,0.596-0.208,1.102-0.625,1.518c-0.416,0.417-0.923,0.625-1.518,0.625h-21.429c-0.596,0-1.102-0.208-1.518-0.625
|
||||
C103.708,124.459,103.5,123.953,103.5,123.357z M110.643,108.357h11.429v-4.286c0-1.577-0.558-2.924-1.674-4.04
|
||||
s-2.463-1.674-4.04-1.674c-1.578,0-2.924,0.558-4.041,1.674c-1.115,1.116-1.674,2.463-1.674,4.04V108.357z"/>
|
||||
<polyline fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" points="99.889,122.5 21.5,122.5 21.5,20.5
|
||||
123.5,20.5 123.5,92.469 "/>
|
||||
<rect x="32.214" y="61.422" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="92.63" y="61.422" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="62.422" y="61.422" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="32.214" y="31.874" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="92.63" y="31.874" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="62.422" y="31.874" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<rect x="32.214" y="91.909" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
<g>
|
||||
<path fill="#4CA0D8" d="M101.579,106.437c0.361-0.361,0.756-0.666,1.179-0.913v-1.452c0-3.714,1.346-6.943,4-9.599
|
||||
c1.073-1.073,2.244-1.921,3.499-2.564H92.63v20.156H99.9V110.5C99.9,108.94,100.48,107.535,101.579,106.437z"/>
|
||||
</g>
|
||||
<rect x="62.422" y="91.909" fill="#4CA0D8" width="20.156" height="20.156"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4>Secure, Private Storage</h4>
|
||||
<p>Containers often contain keys and passwords — take control of your registry by running it behind your firewall on <a href="https://coreos.com/products/managed-linux" data-category="Products: Managed Linux" data-event="Product: Managed Linux">CoreOS Managed Linux</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-sm-3 col-sm-push-9 enterprise-icon">
|
||||
<span class="fa-stack fa-5x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-bar-chart-o fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-9 col-sm-pull-3">
|
||||
<div class="tour-section-title">Auditing and insight done right</div>
|
||||
<div class="tour-section-description">
|
||||
Our platform has built in logging and insight tools, allowing you to see who performed every action on the contents and access control settings of your repositories. This will help you pass internal auditing and compliance requirements more easily.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-sm-3 enterprise-icon">
|
||||
<span class="fa-stack fa-5x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-users fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="tour-section-title">Organize your repositories like you organize your business</div>
|
||||
<div class="tour-section-description">
|
||||
The built in teams and organizations features of <span class="registry-name"></span> allow you to finely control how the different teams and projects within your enterprise collaborate on repositories.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-action">
|
||||
<a href="mailto:support@devtable.com?Subject=Enterprise%20Quay.io%20Inquiry" data-title="Email us to find out more." bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-success">
|
||||
Contact Us
|
||||
</button>
|
||||
</a>
|
||||
<div class="row" style="text-align: center; margin-bottom: 20px;">
|
||||
<a href="https://coreos.com/products/enterprise-registry" class="btn btn-primary">Learn more about Enterprise Registry</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
48
static/img/black-horizontal.svg
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="325.68px" height="112.574px" viewBox="0 0 325.68 112.574" enable-background="new 0 0 325.68 112.574"
|
||||
xml:space="preserve">
|
||||
<path d="M95.701,60.164c0,27.658,9.385,30.316,24.336,30.316c14.95-0.083,24.502-3.488,24.502-30.316
|
||||
c0-28.489-10.133-31.229-24.502-31.229C105.916,28.934,95.701,31.675,95.701,60.164 M107.329,60.164
|
||||
c0-19.352,3.738-20.515,12.708-20.515c9.22,0,12.874,1.578,12.874,20.515c0,16.363-3.073,19.602-12.874,19.602
|
||||
C109.654,79.766,107.329,76.775,107.329,60.164z M116.465,92.889c0.665,10.632,11.711,13.538,20.1,11.711v-9.385
|
||||
c-4.9,1.329-10.881,2.076-13.538-2.326H116.465z"/>
|
||||
<path d="M150.438,75.364c0,8.472,2.907,15.532,13.123,15.532c6.727,0,9.635-2.824,14.037-5.399l2.243,4.651h9.386V46.044h-11.628
|
||||
v31.645c-3.405,2.658-5.98,3.821-10.631,3.821c-2.907,0-4.901-1.495-4.901-5.897V46.044h-11.628V75.364z"/>
|
||||
<path d="M195.457,78.935c0,5.731,3.987,11.462,12.459,11.711c6.728,0.166,12.957-4.402,13.455-5.316l2.409,4.818h9.22V59.417
|
||||
c0-10.964-6.479-14.203-15.947-14.203c-8.223,0-14.286,0.665-20.1,2.907v7.226c3.571-0.249,14.618-0.914,18.522-0.914
|
||||
c3.654,0,5.897,0.665,5.897,5.399c0.083,0.083,0,2.824,0,2.824c0,0.083-12.957,0-12.957,0c-9.469,0-12.957,5.648-12.957,12.127
|
||||
V78.935z M207.085,77.274v-2.16c0-1.91,0.499-4.236,3.572-4.485l10.714-0.831v8.057c0,0-5.316,3.488-10.133,3.488
|
||||
C209.078,81.344,207.085,80.929,207.085,77.274z"/>
|
||||
<path d="M236.573,46.044l8.638,32.891c2.159,8.14,6.977,11.213,13.123,11.213l-6.229,22.426h8.306l6.063-8.472l15.283-58.058H269.63
|
||||
l-8.804,34.137c-0.664-0.083-3.489,0.249-4.402-3.239l-7.641-30.898H236.573z"/>
|
||||
<path d="M283.668,89.321c0,0.621,0.443,1.021,1.065,1.021h3.815c0.621,0,1.109-0.222,1.109-1.021v-3.992
|
||||
c0-0.71-0.311-1.109-1.109-1.109h-3.815c-0.71,0-1.065,0.444-1.065,1.109V89.321z"/>
|
||||
<path d="M294.138,63.281c0,0.621,0.444,1.021,1.065,1.021h4.658c0.621,0,0.976-0.355,0.976-1.021v-3.904
|
||||
c0-0.621-0.311-1.153-0.976-1.153h-4.658c-0.71,0-1.065,0.444-1.065,1.153V63.281z M294.36,90.342h6.211V66.785h-6.211V90.342z"/>
|
||||
<path d="M304.208,78.896c0,8.429,2.617,11.667,10.736,11.667c8.251,0,10.735-2.928,10.735-11.667
|
||||
c0-9.361-2.617-12.333-10.735-12.333C307.003,66.563,304.208,69.27,304.208,78.896 M310.419,78.896c0-6.61,0.843-7.364,4.525-7.364
|
||||
c3.726,0,4.525,0.754,4.525,7.364c0,6.21-1.02,6.654-4.525,6.654C311.661,85.55,310.419,84.974,310.419,78.896z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.505,0c13.241,0,24.037,13.64,24.588,30.737h-8.876
|
||||
c0-11.805-7.035-21.374-15.712-21.374c-8.678,0-15.713,9.569-15.713,21.374h-8.876C14.467,13.64,25.264,0,38.505,0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.504,57.156c2.483,0,4.495,2.013,4.495,4.495c0,0.946-0.292,1.823-0.791,2.547
|
||||
l2.294,12.511H32.507l2.294-12.511c-0.499-0.724-0.791-1.601-0.791-2.547C34.009,59.168,36.022,57.156,38.504,57.156"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.394,34.801h4.724c1.317,0,2.394,1.077,2.394,2.394v59.474
|
||||
c0,1.317-1.077,2.394-2.394,2.394H2.394C1.077,99.063,0,97.986,0,96.669V37.195C0,35.878,1.077,34.801,2.394,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.314,34.801h4.724c1.316,0,2.394,1.077,2.394,2.394v59.474
|
||||
c0,1.317-1.077,2.394-2.394,2.394h-4.724c-1.316,0-2.394-1.077-2.394-2.394V37.195C13.92,35.878,14.997,34.801,16.314,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.183,34.801h4.724c1.317,0,2.394,1.077,2.394,2.394V51.96
|
||||
c0,1.317-1.077,2.394-2.394,2.394h-4.724c-1.317,0-2.394-1.077-2.394-2.394V37.195C26.789,35.878,27.866,34.801,29.183,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.103,34.801h4.724c1.316,0,2.393,1.077,2.393,2.394V51.96
|
||||
c0,1.317-1.077,2.394-2.393,2.394h-4.724c-1.316,0-2.394-1.077-2.394-2.394V37.195C40.709,35.878,41.786,34.801,43.103,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.971,34.801h4.724c1.317,0,2.394,1.077,2.394,2.394v59.474
|
||||
c0,1.317-1.077,2.394-2.394,2.394h-4.724c-1.316,0-2.393-1.077-2.393-2.394V37.195C53.578,35.878,54.655,34.801,55.971,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.891,34.801h4.724c1.317,0,2.394,1.077,2.394,2.394v59.474
|
||||
c0,1.317-1.077,2.394-2.394,2.394h-4.724c-1.317,0-2.394-1.077-2.394-2.394V37.195C67.498,35.878,68.575,34.801,69.891,34.801"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.183,79.51h4.724c1.317,0,2.394,1.077,2.394,2.394V96.67
|
||||
c0,1.317-1.077,2.394-2.394,2.394h-4.724c-1.317,0-2.394-1.077-2.394-2.394V81.904C26.789,80.588,27.866,79.51,29.183,79.51"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.103,79.51h4.724c1.316,0,2.393,1.077,2.393,2.394V96.67
|
||||
c0,1.317-1.077,2.394-2.393,2.394h-4.724c-1.316,0-2.394-1.077-2.394-2.394V81.904C40.709,80.588,41.786,79.51,43.103,79.51"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 103 KiB |
BIN
static/img/coreos-globe-color-lg.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
51
static/img/coreos-wordmark-horiz-white.svg
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="792px" height="306px" viewBox="0 0 792 306" enable-background="new 0 0 792 306" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M136.168,45.527C76.898,45.527,28.689,93.739,28.689,153c0,59.265,48.209,107.474,107.479,107.474
|
||||
c59.252,0,107.465-48.209,107.465-107.474C243.633,93.739,195.42,45.527,136.168,45.527z M176.542,180.428
|
||||
c-0.889,0.129-1.767,0.254-2.659,0.368c-8.111,1.049-16.77,1.761-25.819,2.099c-3.895,0.145-7.872,0.233-11.895,0.233
|
||||
c-4.035,0-8-0.087-11.907-0.233c-9.038-0.338-17.703-1.05-25.807-2.099c-0.723-6.047-1.213-12.339-1.458-18.811
|
||||
c-0.117-2.956-0.175-5.956-0.175-8.985c0-3.032,0.058-6.026,0.175-8.988c0.245-6.473,0.735-12.761,1.458-18.808
|
||||
c0.145-1.259,0.309-2.51,0.478-3.746c5.294-38.424,19.952-66.069,37.235-66.069c53.903,0,97.608,43.699,97.609,97.611
|
||||
C233.777,165.196,210.296,175.688,176.542,180.428z"/>
|
||||
</g>
|
||||
<path fill="#FFFFFF" d="M176.541,125.569c-0.979-1.428-2.029-2.796-3.148-4.11c-8.956-10.557-22.297-17.265-37.224-17.265
|
||||
c-4.839,0-9.148,7.407-11.907,18.909c-1.096,4.586-1.947,9.819-2.495,15.498c-0.432,4.551-0.665,9.391-0.665,14.399
|
||||
s0.233,9.849,0.665,14.396c4.554,0.432,9.387,0.664,14.402,0.664c5.009,0,9.842-0.232,14.396-0.664
|
||||
c10.011-0.95,18.653-2.875,24.775-5.411c6.046-2.501,9.624-5.615,9.624-8.985C184.963,142.832,181.858,133.388,176.541,125.569z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M344.891,100.053c12.585,0,22.816,6.138,29.262,13.062l-10.064,11.326
|
||||
c-5.353-5.192-11.175-8.495-19.041-8.495c-16.839,0-28.953,14.16-28.953,37.291c0,23.448,11.169,37.608,28.32,37.608
|
||||
c9.128,0,15.895-3.775,21.717-10.228l10.067,11.169c-8.335,9.598-19.038,14.95-32.099,14.95c-26.119,0-46.731-18.88-46.731-53.025
|
||||
C297.37,120.036,318.454,100.053,344.891,100.053z"/>
|
||||
<path fill="#FFFFFF" d="M416.961,125.701c19.352,0,36.822,14.793,36.822,40.597c0,25.647-17.471,40.439-36.822,40.439
|
||||
c-19.197,0-36.66-14.792-36.66-40.439C380.301,140.494,397.764,125.701,416.961,125.701z M416.961,191.945
|
||||
c11.33,0,18.25-10.228,18.25-25.647c0-15.577-6.92-25.804-18.25-25.804s-18.094,10.227-18.094,25.804
|
||||
C398.867,181.717,405.631,191.945,416.961,191.945z"/>
|
||||
<path fill="#FFFFFF" d="M459.771,127.589h14.943l1.26,13.688h0.629c5.506-10.07,13.691-15.577,21.871-15.577
|
||||
c3.938,0,6.455,0.472,8.811,1.574l-3.148,15.734c-2.67-0.784-4.717-1.257-8.018-1.257c-6.139,0-13.539,4.245-18.256,15.893v47.203
|
||||
h-18.092L459.771,127.589L459.771,127.589z"/>
|
||||
<path fill="#FFFFFF" d="M541.121,125.701c20.928,0,31.941,15.107,31.941,36.667c0,3.458-0.314,6.604-0.787,8.495h-49.09
|
||||
c1.57,14.003,10.379,21.869,22.811,21.869c6.613,0,12.273-2.041,17.941-5.662l6.135,11.326
|
||||
c-7.395,4.878-16.676,8.341-26.432,8.341c-21.404,0-38.08-14.95-38.08-40.439C505.561,141.12,523.023,125.701,541.121,125.701z
|
||||
M557.326,159.376c0-12.277-5.189-19.671-15.732-19.671c-9.125,0-16.996,6.768-18.57,19.671H557.326z"/>
|
||||
<path fill="#FFFFFF" d="M600.602,152.607c0-32.729,17.785-53.344,42.799-53.344c24.863,0,42.641,20.615,42.641,53.344
|
||||
c0,32.889-17.777,54.13-42.641,54.13C618.387,206.737,600.602,185.496,600.602,152.607z M678.49,152.607
|
||||
c0-28.639-14.158-46.731-35.09-46.731c-21.084,0-35.248,18.093-35.248,46.731c0,28.796,14.164,47.521,35.248,47.521
|
||||
C664.332,200.128,678.49,181.403,678.49,152.607z"/>
|
||||
<path fill="#FFFFFF" d="M699.738,186.125c7.557,8.495,18.412,14.003,30.529,14.003c15.732,0,25.807-8.499,25.807-20.767
|
||||
c0-12.904-8.494-17.154-18.723-21.717l-15.736-7.082c-8.969-3.936-20.934-10.385-20.934-25.808
|
||||
c0-14.947,12.904-25.492,30.059-25.492c12.588,0,22.658,5.665,28.949,12.435l-4.244,4.878c-5.982-6.452-14.32-10.7-24.705-10.7
|
||||
c-13.691,0-22.816,7.239-22.816,18.565c0,11.962,10.385,16.521,17.936,19.985l15.738,6.921
|
||||
c11.486,5.195,21.713,11.647,21.713,27.539s-13.061,27.851-33.201,27.851c-15.107,0-26.75-6.451-34.932-15.576L699.738,186.125z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
49
static/img/coreos.svg
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="792px" height="306px" viewBox="0 0 792 306" enable-background="new 0 0 792 306" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#53A3DA" d="M136.168,45.527C76.898,45.527,28.689,93.739,28.689,153c0,59.265,48.209,107.474,107.479,107.474
|
||||
c59.252,0,107.465-48.209,107.465-107.474C243.633,93.739,195.42,45.527,136.168,45.527z"/>
|
||||
<path fill="#F1606D" d="M136.168,55.389c-17.283,0-31.941,27.645-37.235,66.069c-0.169,1.236-0.333,2.487-0.478,3.746
|
||||
c-0.723,6.047-1.213,12.335-1.458,18.808c-0.117,2.962-0.175,5.956-0.175,8.988c0,3.029,0.058,6.029,0.175,8.985
|
||||
c0.245,6.472,0.735,12.764,1.458,18.811c8.104,1.049,16.769,1.761,25.807,2.099c3.907,0.146,7.872,0.233,11.907,0.233
|
||||
c4.023,0,8-0.088,11.895-0.233c9.049-0.338,17.708-1.05,25.819-2.099c0.892-0.114,1.77-0.239,2.659-0.368
|
||||
c33.754-4.74,57.235-15.232,57.235-27.428C233.776,99.088,190.071,55.389,136.168,55.389z"/>
|
||||
<path fill="#FFFFFF" d="M176.541,125.569c-0.979-1.428-2.029-2.796-3.148-4.11c-8.956-10.557-22.297-17.265-37.224-17.265
|
||||
c-4.839,0-9.148,7.407-11.907,18.909c-1.096,4.586-1.947,9.819-2.495,15.498c-0.432,4.551-0.665,9.391-0.665,14.399
|
||||
s0.233,9.849,0.665,14.396c4.554,0.432,9.387,0.664,14.402,0.664c5.009,0,9.842-0.232,14.396-0.664
|
||||
c10.011-0.95,18.653-2.875,24.775-5.411c6.046-2.501,9.624-5.615,9.624-8.985C184.963,142.832,181.858,133.388,176.541,125.569z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#231F20" d="M344.891,100.053c12.585,0,22.816,6.138,29.262,13.062l-10.064,11.326
|
||||
c-5.353-5.192-11.175-8.495-19.041-8.495c-16.839,0-28.953,14.16-28.953,37.291c0,23.448,11.169,37.608,28.32,37.608
|
||||
c9.128,0,15.895-3.775,21.717-10.228l10.067,11.169c-8.335,9.598-19.038,14.95-32.099,14.95c-26.119,0-46.731-18.88-46.731-53.025
|
||||
C297.37,120.036,318.454,100.053,344.891,100.053z"/>
|
||||
<path fill="#231F20" d="M416.961,125.701c19.352,0,36.822,14.793,36.822,40.597c0,25.647-17.471,40.439-36.822,40.439
|
||||
c-19.197,0-36.66-14.792-36.66-40.439C380.301,140.494,397.764,125.701,416.961,125.701z M416.961,191.945
|
||||
c11.33,0,18.25-10.228,18.25-25.647c0-15.577-6.92-25.804-18.25-25.804s-18.094,10.227-18.094,25.804
|
||||
C398.867,181.717,405.631,191.945,416.961,191.945z"/>
|
||||
<path fill="#231F20" d="M459.771,127.589h14.943l1.26,13.688h0.629c5.506-10.07,13.691-15.577,21.871-15.577
|
||||
c3.938,0,6.455,0.472,8.811,1.574l-3.148,15.734c-2.67-0.784-4.717-1.257-8.018-1.257c-6.139,0-13.539,4.245-18.256,15.893v47.203
|
||||
h-18.092V127.589z"/>
|
||||
<path fill="#231F20" d="M541.121,125.701c20.928,0,31.941,15.107,31.941,36.667c0,3.458-0.314,6.604-0.787,8.495h-49.09
|
||||
c1.57,14.003,10.379,21.869,22.811,21.869c6.613,0,12.273-2.041,17.941-5.662l6.135,11.326
|
||||
c-7.395,4.878-16.676,8.341-26.432,8.341c-21.404,0-38.08-14.95-38.08-40.439C505.561,141.12,523.023,125.701,541.121,125.701z
|
||||
M557.326,159.376c0-12.277-5.189-19.671-15.732-19.671c-9.125,0-16.996,6.768-18.57,19.671H557.326z"/>
|
||||
<path fill="#F1606D" d="M600.602,152.607c0-32.729,17.785-53.344,42.799-53.344c24.863,0,42.641,20.615,42.641,53.344
|
||||
c0,32.889-17.777,54.13-42.641,54.13C618.387,206.737,600.602,185.496,600.602,152.607z M678.49,152.607
|
||||
c0-28.639-14.158-46.731-35.09-46.731c-21.084,0-35.248,18.093-35.248,46.731c0,28.796,14.164,47.521,35.248,47.521
|
||||
C664.332,200.128,678.49,181.403,678.49,152.607z"/>
|
||||
<path fill="#53A4D9" d="M699.738,186.125c7.557,8.495,18.412,14.003,30.529,14.003c15.732,0,25.807-8.499,25.807-20.767
|
||||
c0-12.904-8.494-17.154-18.723-21.717l-15.736-7.082c-8.969-3.936-20.934-10.385-20.934-25.808
|
||||
c0-14.947,12.904-25.492,30.059-25.492c12.588,0,22.658,5.665,28.949,12.435l-4.244,4.878c-5.982-6.452-14.32-10.7-24.705-10.7
|
||||
c-13.691,0-22.816,7.239-22.816,18.565c0,11.962,10.385,16.521,17.936,19.985l15.738,6.921
|
||||
c11.486,5.195,21.713,11.647,21.713,27.539s-13.061,27.851-33.201,27.851c-15.107,0-26.75-6.451-34.932-15.576L699.738,186.125z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/img/image-view.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
static/img/network-tile.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 81 KiB |
606
static/js/app.js
|
@ -1,6 +1,46 @@
|
|||
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||||
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) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
|
@ -59,18 +99,8 @@ function getFirstTextLine(commentString) {
|
|||
}
|
||||
|
||||
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
|
||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
|
||||
bootbox.dialog({
|
||||
"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"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
|
||||
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
|
||||
}
|
||||
|
||||
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
||||
|
@ -84,18 +114,8 @@ function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
|||
'teamname': teamname
|
||||
};
|
||||
|
||||
ApiService.updateOrganizationTeam(data, params).then(callback, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The team could not be created',
|
||||
"title": "Cannot create team",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ApiService.updateOrganizationTeam(data, params)
|
||||
.then(callback, ApiService.errorDisplay('Cannot create team'));
|
||||
}
|
||||
|
||||
function getMarkedDown(string) {
|
||||
|
@ -121,7 +141,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
* pauses in the UI for ngRepeat's when the array is significant in size.
|
||||
*/
|
||||
$provide.factory('AngularViewArray', ['$interval', function($interval) {
|
||||
var ADDTIONAL_COUNT = 50;
|
||||
var ADDTIONAL_COUNT = 20;
|
||||
|
||||
function _ViewArray() {
|
||||
this.isVisible = false;
|
||||
|
@ -364,7 +384,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
var uiService = {};
|
||||
|
||||
uiService.hidePopover = function(elem) {
|
||||
var popover = $('#signupButton').data('bs.popover');
|
||||
var popover = $(elem).data('bs.popover');
|
||||
if (popover) {
|
||||
popover.hide();
|
||||
}
|
||||
|
@ -398,15 +418,19 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
var utilService = {};
|
||||
|
||||
utilService.textToSafeHtml = function(text) {
|
||||
|
||||
utilService.escapeHtmlString = function(text) {
|
||||
var adjusted = text.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
return $sanitize(adjusted);
|
||||
return adjusted;
|
||||
};
|
||||
|
||||
utilService.textToSafeHtml = function(text) {
|
||||
return $sanitize(utilService.escapeHtmlString(text));
|
||||
};
|
||||
|
||||
return utilService;
|
||||
|
@ -417,6 +441,29 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
var pingService = {};
|
||||
var pingCache = {};
|
||||
|
||||
var invokeCallback = function($scope, pings, callback) {
|
||||
if (pings[0] == -1) {
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
callback(-1, false, -1);
|
||||
});
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var sum = 0;
|
||||
for (var i = 0; i < pings.length; ++i) {
|
||||
sum += pings[i];
|
||||
}
|
||||
|
||||
// Report the average ping.
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
callback(Math.floor(sum / pings.length), true, pings.length);
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var reportPingResult = function($scope, url, ping, callback) {
|
||||
// Lookup the cached ping data, if any.
|
||||
var cached = pingCache[url];
|
||||
|
@ -429,28 +476,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
// If an error occurred, report it and done.
|
||||
if (ping < 0) {
|
||||
cached['pings'] = [-1];
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
callback(-1, false, -1);
|
||||
});
|
||||
}, 0);
|
||||
invokeCallback($scope, pings, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, add the current ping and determine the average.
|
||||
cached['pings'].push(ping);
|
||||
|
||||
var sum = 0;
|
||||
for (var i = 0; i < cached['pings'].length; ++i) {
|
||||
sum += cached['pings'][i];
|
||||
}
|
||||
|
||||
// Report the average ping.
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
callback(Math.floor(sum / cached['pings'].length), true, cached['pings'].length);
|
||||
});
|
||||
}, 0);
|
||||
// Invoke the callback.
|
||||
invokeCallback($scope, cached['pings'], callback);
|
||||
|
||||
// Schedule another check if we've done less than three.
|
||||
if (cached['pings'].length < 3) {
|
||||
|
@ -486,12 +520,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
|
||||
pingService.pingUrl = function($scope, url, callback) {
|
||||
if (pingCache[url]) {
|
||||
cached = pingCache[url];
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
callback(cached.result, cached.success);
|
||||
});
|
||||
}, 0);
|
||||
invokeCallback($scope, pingCache[url]['pings'], callback);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -526,7 +555,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return builderService;
|
||||
}]);
|
||||
|
||||
$provide.factory('StringBuilderService', ['$sce', function($sce) {
|
||||
$provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
|
||||
var stringBuilderService = {};
|
||||
|
||||
stringBuilderService.buildString = function(value_or_func, metadata) {
|
||||
|
@ -581,6 +610,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
if (key.indexOf('image') >= 0) {
|
||||
value = value.substr(0, 12);
|
||||
}
|
||||
|
||||
var safe = UtilService.escapeHtmlString(value);
|
||||
var markedDown = getMarkedDown(value);
|
||||
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
||||
|
||||
|
@ -589,7 +620,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
|
||||
}
|
||||
|
||||
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
|
||||
description = description.replace('{' + key + '}', '<code title="' + safe + '">' + markedDown + '</code>');
|
||||
}
|
||||
}
|
||||
return $sce.trustAsHtml(description.replace('\n', '<br>'));
|
||||
|
@ -682,7 +713,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return config;
|
||||
}]);
|
||||
|
||||
$provide.factory('ApiService', ['Restangular', function(Restangular) {
|
||||
$provide.factory('ApiService', ['Restangular', '$q', function(Restangular, $q) {
|
||||
var apiService = {};
|
||||
|
||||
var getResource = function(path, opt_background) {
|
||||
|
@ -779,6 +810,65 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
}
|
||||
};
|
||||
|
||||
var freshLoginFailCheck = function(opName, opArgs) {
|
||||
return function(resp) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// If the error is a fresh login required, show the dialog.
|
||||
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
|
||||
bootbox.dialog({
|
||||
"message": 'It has been more than a few minutes since you last logged in, ' +
|
||||
'so please verify your password to perform this sensitive operation:' +
|
||||
'<form style="margin-top: 10px" action="javascript:void(0)">' +
|
||||
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
|
||||
'</form>',
|
||||
"title": 'Please Verify',
|
||||
"buttons": {
|
||||
"verify": {
|
||||
"label": "Verify",
|
||||
"className": "btn-success",
|
||||
"callback": function() {
|
||||
var info = {
|
||||
'password': $('#freshPassword').val()
|
||||
};
|
||||
|
||||
$('#freshPassword').val('');
|
||||
|
||||
// Conduct the sign in of the user.
|
||||
apiService.verifyUser(info).then(function() {
|
||||
// On success, retry the operation. if it succeeds, then resolve the
|
||||
// deferred promise with the result. Otherwise, reject the same.
|
||||
apiService[opName].apply(apiService, opArgs).then(function(resp) {
|
||||
deferred.resolve(resp);
|
||||
}, function(resp) {
|
||||
deferred.reject(resp);
|
||||
});
|
||||
}, function(resp) {
|
||||
// Reject with the sign in error.
|
||||
deferred.reject({'data': {'message': 'Invalid verification credentials'}});
|
||||
});
|
||||
}
|
||||
},
|
||||
"close": {
|
||||
"label": "Cancel",
|
||||
"className": "btn-default",
|
||||
"callback": function() {
|
||||
deferred.reject(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return a new promise. We'll accept or reject it based on the result
|
||||
// of the login.
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Otherwise, we just 'raise' the error via the reject method on the promise.
|
||||
return $q.reject(resp);
|
||||
};
|
||||
};
|
||||
|
||||
var buildMethodsForOperation = function(operation, resource, resourceMap) {
|
||||
var method = operation['method'].toLowerCase();
|
||||
var operationName = operation['nickname'];
|
||||
|
@ -792,7 +882,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'ignoreLoadingBar': true
|
||||
});
|
||||
}
|
||||
return one['custom' + method.toUpperCase()](opt_options);
|
||||
|
||||
var opObj = one['custom' + method.toUpperCase()](opt_options);
|
||||
|
||||
// If the operation requires_fresh_login, then add a specialized error handler that
|
||||
// will defer the operation's result if sudo is requested.
|
||||
if (operation['requires_fresh_login']) {
|
||||
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
|
||||
}
|
||||
return opObj;
|
||||
};
|
||||
|
||||
// If the method for the operation is a GET, add an operationAsResource method.
|
||||
|
@ -841,6 +939,38 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
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;
|
||||
}]);
|
||||
|
||||
|
@ -1097,7 +1227,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'user': null,
|
||||
'notifications': [],
|
||||
'notificationClasses': [],
|
||||
'notificationSummaries': []
|
||||
'notificationSummaries': [],
|
||||
'additionalNotifications': false
|
||||
};
|
||||
|
||||
var pollTimerHandle = null;
|
||||
|
@ -1193,7 +1324,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'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);
|
||||
if (index >= 0) {
|
||||
|
@ -1250,6 +1383,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
|
||||
ApiService.listUserNotifications().then(function(resp) {
|
||||
notificationService.notifications = resp['notifications'];
|
||||
notificationService.additionalNotifications = resp['additional'];
|
||||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||||
});
|
||||
};
|
||||
|
@ -1512,7 +1646,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 (callbacks['started']) {
|
||||
|
@ -1525,7 +1659,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
planService.getCardInfo(orgname, function(cardInfo) {
|
||||
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1598,9 +1732,34 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
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 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']) {
|
||||
callbacks['opening']();
|
||||
}
|
||||
|
@ -1693,7 +1852,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
|
||||
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
|
||||
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
|
||||
when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
|
||||
reloadOnSearch: false, controller: UserAdminCtrl}).
|
||||
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
|
||||
|
@ -1819,6 +1978,26 @@ quayApp.directive('quayShow', function($animate, Features, Config) {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('quaySection', function($animate, $location, $rootScope) {
|
||||
return {
|
||||
priority: 590,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
var update = function() {
|
||||
var result = $location.path().indexOf('/' + $attr.quaySection) == 0;
|
||||
$animate[!result ? 'removeClass' : 'addClass']($element, 'active');
|
||||
};
|
||||
|
||||
$scope.$watch(function(){
|
||||
return $location.path();
|
||||
}, update);
|
||||
|
||||
$scope.$watch($attr.quaySection, update);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('quayClasses', function(Features, Config) {
|
||||
return {
|
||||
priority: 580,
|
||||
|
@ -2018,18 +2197,7 @@ quayApp.directive('applicationReference', function () {
|
|||
template: '/static/directives/application-reference-dialog.html',
|
||||
show: true
|
||||
});
|
||||
}, function() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Application could not be found'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -2110,6 +2278,8 @@ quayApp.directive('copyBox', function () {
|
|||
'hoveringMessage': '=hoveringMessage'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
$scope.disabled = false;
|
||||
|
||||
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||
$rootScope.__copyBoxIdCounter = number + 1;
|
||||
$scope.inputId = "copy-box-input-" + number;
|
||||
|
@ -2119,27 +2289,7 @@ quayApp.directive('copyBox', function () {
|
|||
|
||||
input.attr('id', $scope.inputId);
|
||||
button.attr('data-clipboard-target', $scope.inputId);
|
||||
|
||||
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);
|
||||
});
|
||||
$scope.disabled = !button.clipboardCopy();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -2194,7 +2344,7 @@ quayApp.directive('externalLoginButton', function () {
|
|||
'provider': '@provider',
|
||||
'action': '@action'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
|
||||
$scope.startSignin = function(service) {
|
||||
$scope.signInStarted({'service': service});
|
||||
|
||||
|
@ -2228,15 +2378,39 @@ quayApp.directive('signinForm', function () {
|
|||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||
$scope.tryAgainSoon = 0;
|
||||
$scope.tryAgainInterval = null;
|
||||
|
||||
$scope.markStarted = function() {
|
||||
if ($scope.signInStarted != null) {
|
||||
$scope.signInStarted();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cancelInterval = function() {
|
||||
$scope.tryAgainSoon = 0;
|
||||
|
||||
if ($scope.tryAgainInterval) {
|
||||
$interval.cancel($scope.tryAgainInterval);
|
||||
}
|
||||
|
||||
$scope.tryAgainInterval = null;
|
||||
};
|
||||
|
||||
$scope.$watch('user.username', function() {
|
||||
$scope.cancelInterval();
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$scope.cancelInterval();
|
||||
});
|
||||
|
||||
$scope.signin = function() {
|
||||
if ($scope.tryAgainSoon > 0) { return; }
|
||||
|
||||
$scope.markStarted();
|
||||
$scope.cancelInterval();
|
||||
|
||||
ApiService.signinUser($scope.user).then(function() {
|
||||
$scope.needsEmailVerification = false;
|
||||
|
@ -2258,8 +2432,23 @@ quayApp.directive('signinForm', function () {
|
|||
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
|
||||
}, 500);
|
||||
}, function(result) {
|
||||
$scope.needsEmailVerification = result.data.needsEmailVerification;
|
||||
$scope.invalidCredentials = result.data.invalidCredentials;
|
||||
if (result.status == 429 /* try again later */) {
|
||||
$scope.needsEmailVerification = false;
|
||||
$scope.invalidCredentials = false;
|
||||
|
||||
$scope.cancelInterval();
|
||||
|
||||
$scope.tryAgainSoon = result.headers('Retry-After');
|
||||
$scope.tryAgainInterval = $interval(function() {
|
||||
$scope.tryAgainSoon--;
|
||||
if ($scope.tryAgainSoon <= 0) {
|
||||
$scope.cancelInterval();
|
||||
}
|
||||
}, 1000, $scope.tryAgainSoon);
|
||||
} else {
|
||||
$scope.needsEmailVerification = result.data.needsEmailVerification;
|
||||
$scope.invalidCredentials = result.data.invalidCredentials;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -2370,11 +2559,42 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
|||
'username': '=username',
|
||||
'token': '=token',
|
||||
'shown': '=shown',
|
||||
'counter': '=counter'
|
||||
'counter': '=counter',
|
||||
'supportsRegenerate': '@supportsRegenerate',
|
||||
'regenerate': '®enerate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
controller: function($scope, $element) {
|
||||
var updateCommand = function() {
|
||||
var escape = function(v) {
|
||||
if (!v) { return v; }
|
||||
return v.replace('$', '\\$');
|
||||
};
|
||||
$scope.command = 'docker login -e="." -u="' + escape($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() {
|
||||
try { return !!new Blob(); } catch(e){}
|
||||
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) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
@ -2392,6 +2612,8 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
|||
};
|
||||
|
||||
var show = function(r) {
|
||||
$scope.regenerating = false;
|
||||
|
||||
if (!$scope.shown || !$scope.username || !$scope.token) {
|
||||
$('#dockerauthmodal').modal('hide');
|
||||
return;
|
||||
|
@ -2632,6 +2854,8 @@ quayApp.directive('logsView', function () {
|
|||
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||
},
|
||||
|
||||
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
||||
|
||||
// Note: These are deprecated.
|
||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||
|
@ -2675,6 +2899,7 @@ quayApp.directive('logsView', function () {
|
|||
'reset_application_client_secret': 'Reset Client Secret',
|
||||
'add_repo_notification': 'Add repository notification',
|
||||
'delete_repo_notification': 'Delete repository notification',
|
||||
'regenerate_robot_token': 'Regenerate Robot Token',
|
||||
|
||||
// Note: these are deprecated.
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
|
@ -2801,18 +3026,7 @@ quayApp.directive('applicationManager', function () {
|
|||
|
||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||
$scope.applications.push(resp);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp['message'] || 'The application could not be created',
|
||||
"title": "Cannot create application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot create application'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -2857,6 +3071,20 @@ quayApp.directive('robotsManager', function () {
|
|||
$scope.shownRobot = null;
|
||||
$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.shownRobot = info;
|
||||
$scope.showRobotCounter++;
|
||||
|
@ -2897,18 +3125,7 @@ quayApp.directive('robotsManager', function () {
|
|||
if (index >= 0) {
|
||||
$scope.robots.splice(index, 1);
|
||||
}
|
||||
}, function() {
|
||||
bootbox.dialog({
|
||||
"message": 'The selected robot account could not be deleted',
|
||||
"title": "Cannot delete robot account",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete robot account'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -2973,18 +3190,7 @@ quayApp.directive('prototypeManager', function () {
|
|||
|
||||
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
prototype.role = role;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The permission could not be modified',
|
||||
"title": "Cannot modify permission",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot modify permission'));
|
||||
};
|
||||
|
||||
$scope.comparePrototypes = function(p) {
|
||||
|
@ -3024,23 +3230,16 @@ quayApp.directive('prototypeManager', function () {
|
|||
data['activating_user'] = $scope.activatingForNew;
|
||||
}
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create permission',
|
||||
function(resp) {
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
$scope.prototypes.push(resp);
|
||||
$scope.loading = false;
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
}, function(resp) {
|
||||
$('#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"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.deletePrototype = function(prototype) {
|
||||
|
@ -3054,18 +3253,7 @@ quayApp.directive('prototypeManager', function () {
|
|||
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
|
||||
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
|
||||
$scope.loading = false;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The permission could not be deleted',
|
||||
"title": "Cannot delete permission",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete permission'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -3836,9 +4024,11 @@ quayApp.directive('billingOptions', function () {
|
|||
|
||||
var save = function() {
|
||||
$scope.working = true;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not change user details');
|
||||
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
|
||||
$scope.working = false;
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var checkSave = function() {
|
||||
|
@ -3890,7 +4080,7 @@ quayApp.directive('planManager', function () {
|
|||
return true;
|
||||
};
|
||||
|
||||
$scope.changeSubscription = function(planId) {
|
||||
$scope.changeSubscription = function(planId, opt_async) {
|
||||
if ($scope.planChanging) { return; }
|
||||
|
||||
var callbacks = {
|
||||
|
@ -3904,7 +4094,7 @@ quayApp.directive('planManager', function () {
|
|||
}
|
||||
};
|
||||
|
||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
|
||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
|
||||
};
|
||||
|
||||
$scope.cancelSubscription = function() {
|
||||
|
@ -3967,7 +4157,7 @@ quayApp.directive('planManager', function () {
|
|||
if ($scope.readyForPlan) {
|
||||
var planRequested = $scope.readyForPlan();
|
||||
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
||||
$scope.changeSubscription(planRequested);
|
||||
$scope.changeSubscription(planRequested, /* async */true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -3998,7 +4188,7 @@ quayApp.directive('namespaceSelector', function () {
|
|||
'namespace': '=namespace',
|
||||
'requireCreate': '=requireCreate'
|
||||
},
|
||||
controller: function($scope, $element, $routeParams, CookieService) {
|
||||
controller: function($scope, $element, $routeParams, $location, CookieService) {
|
||||
$scope.namespaces = {};
|
||||
|
||||
$scope.initialize = function(user) {
|
||||
|
@ -4035,6 +4225,10 @@ quayApp.directive('namespaceSelector', function () {
|
|||
|
||||
if (newNamespace) {
|
||||
CookieService.putPermanent('quay.namespace', newNamespace);
|
||||
|
||||
if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
|
||||
$location.search({'namespace': newNamespace});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -4385,26 +4579,17 @@ quayApp.directive('setupTriggerDialog', function () {
|
|||
|
||||
$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) {
|
||||
$scope.hide();
|
||||
$scope.trigger['is_active'] = true;
|
||||
$scope.trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
|
@ -4744,6 +4929,9 @@ quayApp.directive('buildMessage', function () {
|
|||
case 'waiting':
|
||||
return 'Waiting for available build worker';
|
||||
|
||||
case 'unpacking':
|
||||
return 'Unpacking build package';
|
||||
|
||||
case 'pulling':
|
||||
return 'Pulling base image';
|
||||
|
||||
|
@ -4799,6 +4987,7 @@ quayApp.directive('buildProgress', function () {
|
|||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'cannot_load':
|
||||
case 'unpacking':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
@ -5018,6 +5207,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 () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -5329,7 +5535,9 @@ quayApp.directive('locationView', function () {
|
|||
'local_us': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States' },
|
||||
'local_eu': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },
|
||||
|
||||
's3_us_east_1': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States' },
|
||||
's3_us_east_1': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States (East)' },
|
||||
's3_us_west_1': { 'country': 'US', 'data': 'quay-registry-cali.s3.amazonaws.com', 'title': 'United States (West)' },
|
||||
|
||||
's3_eu_west_1': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },
|
||||
|
||||
's3_ap_southeast_1': { 'country': 'SG', 'data': 'quay-registry-singapore.s3-ap-southeast-1.amazonaws.com', 'title': 'Singapore' },
|
||||
|
@ -5344,7 +5552,9 @@ quayApp.directive('locationView', function () {
|
|||
|
||||
$scope.getLocationTooltip = function(location, ping) {
|
||||
var tip = $scope.getLocationTitle(location) + '<br>';
|
||||
if (ping < 0) {
|
||||
if (ping == null) {
|
||||
tip += '(Loading)';
|
||||
} else if (ping < 0) {
|
||||
tip += '<br><b>Note: Could not contact server</b>';
|
||||
} else {
|
||||
tip += 'Estimated Ping: ' + (ping ? ping + 'ms' : '(Loading)');
|
||||
|
@ -5367,7 +5577,7 @@ quayApp.directive('locationView', function () {
|
|||
};
|
||||
|
||||
$scope.getLocationPing = function(location) {
|
||||
var url = 'http://' + LOCATIONS[location]['data'] + '/okay.txt';
|
||||
var url = 'https://' + LOCATIONS[location]['data'] + '/okay.txt';
|
||||
PingService.pingUrl($scope, url, function(ping, success, count) {
|
||||
if (count == 3 || !success) {
|
||||
$scope.locationPing = success ? ping : -1;
|
||||
|
@ -5424,7 +5634,8 @@ quayApp.directive('tagSpecificImagesView', function () {
|
|||
scope: {
|
||||
'repository': '=repository',
|
||||
'tag': '=tag',
|
||||
'images': '=images'
|
||||
'images': '=images',
|
||||
'imageCutoff': '=imageCutoff'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getFirstTextLine = getFirstTextLine;
|
||||
|
@ -5446,7 +5657,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
|||
return classes;
|
||||
};
|
||||
|
||||
var forAllTagImages = function(tag, callback) {
|
||||
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
||||
if (!tag) { return; }
|
||||
|
||||
if (!$scope.imageByDBID) {
|
||||
|
@ -5464,10 +5675,14 @@ quayApp.directive('tagSpecificImagesView', function () {
|
|||
|
||||
callback(tag_image);
|
||||
|
||||
var ancestors = tag_image.ancestors.split('/');
|
||||
var ancestors = tag_image.ancestors.split('/').reverse();
|
||||
for (var i = 0; i < ancestors.length; ++i) {
|
||||
var image = $scope.imageByDBID[ancestors[i]];
|
||||
if (image) {
|
||||
if (image == opt_cutoff) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(image);
|
||||
}
|
||||
}
|
||||
|
@ -5489,7 +5704,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
|||
var ids = {};
|
||||
forAllTagImages(currentTag, function(image) {
|
||||
ids[image.dbid] = true;
|
||||
});
|
||||
}, $scope.imageCutoff);
|
||||
return ids;
|
||||
};
|
||||
|
||||
|
@ -5587,15 +5802,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
|
||||
// Handle session expiration.
|
||||
Restangular.setErrorInterceptor(function(response) {
|
||||
if (response.status == 401) {
|
||||
if (response.data['session_required'] == null || response.data['session_required'] === true) {
|
||||
$('#sessionexpiredModal').modal({});
|
||||
return false;
|
||||
}
|
||||
if (response.status == 401 && response.data['error_type'] == 'invalid_token' &&
|
||||
response.data['session_required'] !== false) {
|
||||
$('#sessionexpiredModal').modal({});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Features.BILLING && response.status == 402) {
|
||||
$('#overlicenseModal').modal({});
|
||||
if (response.status == 503) {
|
||||
$('#cannotContactService').modal({});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,3 @@
|
|||
$.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 GuideCtrl() {
|
||||
}
|
||||
|
||||
|
@ -431,6 +409,27 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
$location.search('current', buildInfo.id);
|
||||
};
|
||||
|
||||
$scope.isPushing = function(images) {
|
||||
if (!images) { return false; }
|
||||
|
||||
var cached = images.__isPushing;
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return images.__isPushing = $scope.isPushingInternal(images);
|
||||
};
|
||||
|
||||
$scope.isPushingInternal = function(images) {
|
||||
if (!images) { return false; }
|
||||
|
||||
for (var i = 0; i < images.length; ++i) {
|
||||
if (images[i].uploading) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.getTooltipCommand = function(image) {
|
||||
var sanitized = ImageMetadataService.getEscapedFormattedCommand(image);
|
||||
return '<span class=\'codetooltip\'>' + sanitized + '</span>';
|
||||
|
@ -511,48 +510,37 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
'image': image.id
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) {
|
||||
$('#addTagModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.changeTagImage(data, params).then(function(resp) {
|
||||
$scope.creatingTag = false;
|
||||
loadViewInfo();
|
||||
$('#addTagModal').modal('hide');
|
||||
}, function(resp) {
|
||||
$('#addTagModal').modal('hide');
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'Could not create or move tag',
|
||||
"title": "Cannot create or move tag",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.deleteTag = function(tagName) {
|
||||
if (!$scope.repo.can_admin) { return; }
|
||||
$('#confirmdeleteTagModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'tag': tagName
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot delete tag', function() {
|
||||
$('#confirmdeleteTagModal').modal('hide');
|
||||
$scope.deletingTag = false;
|
||||
});
|
||||
|
||||
$scope.deletingTag = true;
|
||||
|
||||
ApiService.deleteFullTag(null, params).then(function() {
|
||||
loadViewInfo();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'Could not delete tag',
|
||||
"title": "Cannot delete tag",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#confirmdeleteTagModal').modal('hide');
|
||||
$scope.deletingTag = false;
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.getImagesForTagBySize = function(tag) {
|
||||
|
@ -731,8 +719,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
// Load the builds for this repository. If none are active it will cancel the poll.
|
||||
startBuildInfoTimer(repo);
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1341,17 +1327,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$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));
|
||||
permissionDelete.customDELETE().then(function() {
|
||||
delete $scope.permissions[kind][entityName];
|
||||
}, function(resp) {
|
||||
if (resp.status == 409) {
|
||||
$scope.changePermError = resp.data || '';
|
||||
$('#channgechangepermModal').modal({});
|
||||
} else {
|
||||
$('#cannotchangeModal').modal({});
|
||||
}
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.addRole = function(entityName, role, kind) {
|
||||
|
@ -1362,9 +1347,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionPost.customPUT(permission).then(function(result) {
|
||||
$scope.permissions[kind][entityName] = result;
|
||||
}, function(result) {
|
||||
$('#cannotchangeModal').modal({});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot change permission'));
|
||||
};
|
||||
|
||||
$scope.roles = [
|
||||
|
@ -1579,18 +1562,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
window.console.log(resp);
|
||||
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
||||
document.location = url;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp['message'] || 'The build could not be started',
|
||||
"title": "Could not start build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not start build'));
|
||||
};
|
||||
|
||||
$scope.deleteTrigger = function(trigger) {
|
||||
|
@ -1720,18 +1692,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
|
||||
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not revoke authorization',
|
||||
"title": "Cannot revoke authorization",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not revoke authorization'));
|
||||
};
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
|
@ -1740,7 +1701,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
||||
$scope.invoicesShown++;
|
||||
};
|
||||
|
||||
|
@ -1809,7 +1769,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.changeEmailForm.$setPristine();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1819,7 +1779,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.updatingUser = true;
|
||||
$scope.changePasswordSuccess = false;
|
||||
|
||||
ApiService.changeUserDetails($scope.cuser).then(function() {
|
||||
ApiService.changeUserDetails($scope.cuser).then(function(resp) {
|
||||
|
||||
$scope.updatingUser = false;
|
||||
$scope.changePasswordSuccess = true;
|
||||
|
||||
|
@ -1926,9 +1887,6 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, I
|
|||
|
||||
// Fetch the image's changes.
|
||||
fetchChanges();
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
|
||||
return image;
|
||||
});
|
||||
};
|
||||
|
@ -2196,13 +2154,14 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
|||
'teamname': teamname
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot delete team', function() {
|
||||
$scope.currentDeleteTeam = null;
|
||||
});
|
||||
|
||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||
delete $scope.organization.teams[teamname];
|
||||
$scope.currentDeleteTeam = null;
|
||||
}, function() {
|
||||
$('#cannotchangeModal').modal({});
|
||||
$scope.currentDeleteTeam = null;
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
|
@ -2496,9 +2455,9 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
|||
};
|
||||
|
||||
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
||||
}, function(result) {
|
||||
}, function(resp) {
|
||||
$scope.creating = false;
|
||||
$scope.createError = result.data.error_description || result.data;
|
||||
$scope.createError = ApiService.getErrorMessage(resp);
|
||||
$timeout(function() {
|
||||
$('#orgName').popover('show');
|
||||
});
|
||||
|
@ -2575,18 +2534,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
$timeout(function() {
|
||||
$location.path('/organization/' + orgname + '/admin');
|
||||
}, 500);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not delete application',
|
||||
"title": "Cannot delete application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not delete application'));
|
||||
};
|
||||
|
||||
$scope.updateApplication = function() {
|
||||
|
@ -2604,22 +2552,13 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
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) {
|
||||
$scope.application = resp;
|
||||
$scope.updating = false;
|
||||
}, 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"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.resetClientSecret = function() {
|
||||
|
@ -2632,18 +2571,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
|
||||
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
|
||||
$scope.application = resp;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not reset client secret',
|
||||
"title": "Cannot reset client secret",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not reset client secret'));
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
|
@ -2739,18 +2667,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
|||
|
||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data.message : 'Could not change user',
|
||||
"title": "Cannot change user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not change user'));
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
|
@ -2762,49 +2679,10 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
|||
|
||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data.message : 'Could not delete user',
|
||||
"title": "Cannot delete user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete user'));
|
||||
};
|
||||
|
||||
var seatUsageLoaded = function(usage) {
|
||||
$scope.usageLoading = false;
|
||||
|
||||
if (usage.count > usage.allowed) {
|
||||
$scope.limit = 'over';
|
||||
} else if (usage.count == usage.allowed) {
|
||||
$scope.limit = 'at';
|
||||
} else if (usage.count >= usage.allowed * 0.7) {
|
||||
$scope.limit = 'near';
|
||||
} else {
|
||||
$scope.limit = 'none';
|
||||
}
|
||||
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new UsageChart();
|
||||
$scope.chart.draw('seat-usage-chart');
|
||||
}
|
||||
|
||||
$scope.chart.update(usage.count, usage.allowed);
|
||||
};
|
||||
|
||||
var loadSeatUsage = function() {
|
||||
$scope.usageLoading = true;
|
||||
ApiService.getSeatCount().then(function(resp) {
|
||||
seatUsageLoaded(resp);
|
||||
});
|
||||
};
|
||||
|
||||
loadSeatUsage();
|
||||
$scope.loadUsers();
|
||||
}
|
||||
|
||||
function TourCtrl($scope, $location) {
|
||||
|
|
|
@ -148,6 +148,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
|||
var ch = dimensions.ch;
|
||||
|
||||
// Set the height of the container so that it never goes offscreen.
|
||||
if (!$('#' + container).removeOverscroll) { return; }
|
||||
|
||||
$('#' + container).removeOverscroll();
|
||||
var viewportHeight = $(window).height();
|
||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||
|
@ -402,6 +404,10 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
|||
var roots = [];
|
||||
for (var i = 0; i < this.images_.length; ++i) {
|
||||
var image = this.images_[i];
|
||||
|
||||
// Skip images that are currently uploading.
|
||||
if (image.uploading) { continue; }
|
||||
|
||||
var imageNode = imageByDBID[image.dbid];
|
||||
var ancestors = this.getAncestors_(image);
|
||||
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
||||
|
@ -432,6 +438,10 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
|||
var maxChildCount = roots.length;
|
||||
for (var i = 0; i < this.images_.length; ++i) {
|
||||
var image = this.images_[i];
|
||||
|
||||
// Skip images that are currently uploading.
|
||||
if (image.uploading) { continue; }
|
||||
|
||||
var imageNode = imageByDBID[image.dbid];
|
||||
maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
|
||||
}
|
||||
|
@ -582,6 +592,10 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
|||
// Ensure that the children are in the correct order.
|
||||
for (var i = 0; i < this.images_.length; ++i) {
|
||||
var image = this.images_[i];
|
||||
|
||||
// Skip images that are currently uploading.
|
||||
if (image.uploading) { continue; }
|
||||
|
||||
var imageNode = this.imageByDBID_[image.dbid];
|
||||
var ancestors = this.getAncestors_(image);
|
||||
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
||||
|
|
17
static/lib/ZeroClipboard.min.js
vendored
Executable file → Normal file
BIN
static/lib/ZeroClipboard.swf
Executable file → Normal file
|
@ -7,41 +7,49 @@
|
|||
<div class="col-sm-12 about-basic-info">
|
||||
<h3>The Basics</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-3">
|
||||
<div class="about-basic-icon"><i class="fa fa-3x fa-calendar"></i></div>
|
||||
<div class="about-basic-text">
|
||||
<b> Founded</b><br>
|
||||
2012
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-3">
|
||||
<div class="about-basic-icon"><i class="fa fa-3x fa-globe"></i></div>
|
||||
<div class="about-basic-text">
|
||||
<b> Location</b><br>
|
||||
New York City, NY
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-3">
|
||||
<div class="about-basic-icon"><i class="fa fa-3x fa-users"></i></div>
|
||||
<div class="about-basic-text">
|
||||
<b> Worker Bees</b><br>
|
||||
<b> Local Worker Bees</b><br>
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="about-basic-icon"><img src="/static/img/coreos-globe-color-lg.png" style="height: 42px; vertical-align: bottom;"></div>
|
||||
<div class="about-basic-text">
|
||||
<b> CoreOS</b><br>
|
||||
August, 2014
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>Our Story</h3>
|
||||
<p>Quay.io was originally created out of necessesity when we wanted to use Docker containers with DevTable IDE. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the <a href="http://www.meetup.com/Docker-NewYorkCity/events/142142762/">Docker New York City Meetup</a> on October 2nd, 2013.</p>
|
||||
<p>Since that time, our users have demanded that Quay.io become our main focus. Our customers rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions. We are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be. We thank you for taking this journey with us.</p>
|
||||
<p>Quay.io was originally created out of necessesity when we wanted to use Docker containers with our original IDE product. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the <a href="http://www.meetup.com/Docker-NewYorkCity/events/142142762/">Docker New York City Meetup</a> on October 2nd, 2013.</p>
|
||||
<p>After launch, our customers demanded that Quay.io become our main focus. They rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions.</p>
|
||||
<p>In August, 2014, Quay.io joined <a href="https://coreos.com">CoreOS</a> to provide registry support for the enterprise. As ever, we are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>The Team</h3>
|
||||
Our team is composed of two software engineers turned entrepreneurs:
|
||||
<h3>The Quay.io Team at CoreOS</h3>
|
||||
The Quay.io team is composed of two software engineers:
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -67,12 +75,7 @@
|
|||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/9fc3232622773fb2e8f71c0027601bc5?s=128&d=mm">
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-10">
|
||||
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. Joseph was one of the original duo responsible for inventing the language and framework on which DevTable is built.</p>
|
||||
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<p>With a combined 10 years experience building tools for software engineers, our founding team knows what it takes to make software engineers happy doing their work. Combined with our love for the web, we are ready to make a difference in the way people think about software development in the cloud.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
<div class="landing-background" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
|
||||
<div class="landing-filter" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
|
||||
<div class="landing-content">
|
||||
<img class="logo" src="/static/img/white_horizontal.png">
|
||||
|
||||
<div class="header-bar"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row messages">
|
||||
<div class="col-md-7">
|
||||
|
@ -28,7 +24,7 @@
|
|||
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
<a href="/repository/?namespace={{ user.username }}">See All Repositories</a>
|
||||
<a href="/repository/?namespace={{ namespace }}">See All Repositories</a>
|
||||
</div>
|
||||
|
||||
<!-- No Repos -->
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
<div class="jumbotron landing">
|
||||
<div class="announcement">
|
||||
<span class="hidden-xs-inline">
|
||||
<img src="/static/img/white_horizontal.png" style="height: 40px">
|
||||
<span class="plus">+</span>
|
||||
<img src="/static/img/coreos-wordmark-horiz-white.svg">
|
||||
</span>
|
||||
|
||||
<span class="spacer"></span>
|
||||
Quay.io is now part of CoreOS! <a href="http://blog.devtable.com/" target="_blank">Read the blog post.</a>
|
||||
</div>
|
||||
|
||||
<div class="landing-background" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
|
||||
<div class="landing-filter" ng-class="user.anonymous ? 'landing': 'signedin'"></div>
|
||||
<div class="landing-content">
|
||||
<img class="logo" src="/static/img/white_horizontal.png">
|
||||
|
||||
<div class="header-bar"></div>
|
||||
|
||||
<div class="landing-content">
|
||||
<div class="container">
|
||||
<div class="row messages">
|
||||
<div class="col-md-7">
|
||||
<div ng-show="user.anonymous">
|
||||
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
|
||||
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
||||
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
|
||||
<a href="/plans/?trial-plan=bus-coreos-trial" class="call-to-action">Get 20 free private repos for 6 months<i class="fa fa-arrow-right"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="!user.anonymous">
|
||||
|
@ -27,7 +34,7 @@
|
|||
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
<a href="/repository/?namespace={{ user.username }}">See All Repositories</a>
|
||||
<a href="/repository/?namespace={{ namespace }}">See All Repositories</a>
|
||||
</div>
|
||||
|
||||
<!-- No Repos -->
|
||||
|
@ -265,7 +272,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="container" ng-if="user.anonymous">
|
||||
<div class="row">
|
||||
<div style="border-top: 1px solid #eee; padding-top: 20px;" class="col-md-12">
|
||||
<a href="https://mixpanel.com/f/partner"><img src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png" alt="Mobile Analytics" /></a>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="container create-org" ng-show="!creating">
|
||||
|
||||
<div class="row header-row">
|
||||
<div class="col-md-8 col-md-offset-1">
|
||||
<div class="col-md-12">
|
||||
<h2>Create Organization</h2>
|
||||
|
||||
<div class="steps-container" ng-show="false">
|
||||
|
@ -44,8 +44,7 @@
|
|||
|
||||
<!-- Step 2 -->
|
||||
<div class="row" ng-show="user && !user.anonymous && !created">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-12">
|
||||
<div class="step-container">
|
||||
<h3>Setup the new organization</h3>
|
||||
|
||||
|
@ -54,7 +53,7 @@
|
|||
<label for="orgName">Organization Name</label>
|
||||
<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 }}"
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
@ -85,8 +84,7 @@
|
|||
|
||||
<!-- Step 3 -->
|
||||
<div class="row" ng-show="user && !user.anonymous && created">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-12">
|
||||
<div class="step-container">
|
||||
<h3>Organization Created</h3>
|
||||
<h4><a href="/organization/{{ org.name }}">Manage Teams Now</a></h4>
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
<!-- Header -->
|
||||
<div class="row">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="new-header">
|
||||
<span style="color: #444;">
|
||||
|
@ -45,8 +45,8 @@
|
|||
|
||||
<!-- Private/public -->
|
||||
<div class="row">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="section-title">Repository Visibility</div>
|
||||
<div class="section">
|
||||
<div class="repo-option">
|
||||
|
@ -98,8 +98,8 @@
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="section-title">Initialize repository</div>
|
||||
|
||||
|
@ -137,8 +137,8 @@
|
|||
</div>
|
||||
|
||||
<div class="row" ng-show="repo.initialize == 'dockerfile' || repo.initialize == 'zipfile'">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">Archive</span></div>
|
||||
<div style="padding-top: 20px;">
|
||||
|
@ -153,8 +153,8 @@
|
|||
</div>
|
||||
|
||||
<div class="row" ng-show="repo.initialize == 'github'">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-info">
|
||||
You will be redirected to authorize via GitHub once the repository has been created
|
||||
</div>
|
||||
|
@ -162,8 +162,8 @@
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-large btn-success" type="submit"
|
||||
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || ((repo.initialize == 'dockerfile' || repo.initialize == 'zipfile') && !hasDockerfile)">
|
||||
<i class="fa fa-large" ng-class="repo.is_public == '1' ? 'fa-unlock' : 'fa-lock'" style="margin-right: 4px"></i>
|
||||
|
|
|
@ -34,6 +34,13 @@
|
|||
</span>
|
||||
<i class="fa fa-upload visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
|
@ -50,16 +57,9 @@
|
|||
</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">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="All plans have a 14-day free trial">
|
||||
<span class="hidden-sm-inline">14-Day Free Trial</span>
|
||||
<span class="visible-sm-inline">14-Day Trial</span>
|
||||
data-title="All plans have a free trial">
|
||||
<span class="hidden-sm-inline">Free Trial</span>
|
||||
<span class="visible-sm-inline">Free Trial</span>
|
||||
</span>
|
||||
<i class="fa fa-clock-o visible-lg"></i>
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<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 present"></div>
|
||||
|
@ -93,10 +93,10 @@
|
|||
<div class="feature present">SSL Encryption</div>
|
||||
<div class="feature present">Robot accounts</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'">Logging</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
|
||||
<div class="feature present">14-Day Free Trial</div>
|
||||
<div class="feature present">Free Trial</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-block" ng-class="plan.bus_features ? 'btn-success' : 'btn-primary'"
|
||||
|
@ -128,11 +128,19 @@
|
|||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Enterprise</h3>
|
||||
<dt>I work in an enterprise and we need to run Quay.io on our servers. Can I do so?</dt>
|
||||
<dd>Please contact us at our <a href="mailto:support@quay.io">support email address</a> to discuss enterprise plans.</dd>
|
||||
|
||||
<div class="row enterprise-plan">
|
||||
<div class="col-md-6">
|
||||
<h2>Run Quay.io Behind Your Firewall</h2>
|
||||
<div class="plan-combine">
|
||||
<img src="/static/img/quay-logo.png">
|
||||
<span class="plus">+</span>
|
||||
<img src="/static/img/coreos.svg" style="height: 50px">
|
||||
</div>
|
||||
<div>Quay.io has partnered with CoreOS to offer Enterprise Registry, a version
|
||||
of Quay.io that can be hosted behind your firewall. More information
|
||||
can be found on the <a href="https://coreos.com/products/enterprise-registry">CoreOS website</a>.</div>
|
||||
<a href="https://coreos.com/products/enterprise-registry" class="btn btn-default">Learn more about Enterprise Registry</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#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="#notification" ng-click="loadNotifications()">Notifications</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||
|
@ -225,7 +226,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 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-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>
|
||||
|
@ -377,24 +378,6 @@
|
|||
counter="showNewNotificationCounter"
|
||||
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 -->
|
||||
<div class="modal fade" id="makepublicModal">
|
||||
<div class="modal-dialog">
|
||||
|
@ -441,26 +424,6 @@
|
|||
</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 -->
|
||||
<div class="modal fade" id="confirmdeleteModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -19,19 +16,8 @@
|
|||
<!-- Content -->
|
||||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<!-- License tab -->
|
||||
<div id="license" class="tab-pane active">
|
||||
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
|
||||
<!-- Chart -->
|
||||
<div>
|
||||
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
|
||||
<span class="usage-caption" ng-show="chart">Seat Usage</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Users tab -->
|
||||
<div id="users" class="tab-pane">
|
||||
<div id="users" class="tab-pane active">
|
||||
<div class="quay-spinner" ng-show="!users"></div>
|
||||
<div class="alert alert-error" ng-show="usersError">
|
||||
{{ usersError }}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
|
||||
</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>
|
||||
</li>
|
||||
|
||||
|
@ -138,13 +138,14 @@
|
|||
|
||||
<!-- Change password tab -->
|
||||
<div id="password" class="tab-pane">
|
||||
<div class="loading" ng-show="updatingUser">
|
||||
<div class="quay-spinner 3x"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<div class="panel-title">Change Password</div>
|
||||
|
||||
<div class="loading" ng-show="updatingUser">
|
||||
<div class="quay-spinner 3x"></div>
|
||||
</div>
|
||||
|
||||
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
||||
|
||||
<div ng-show="!updatingUser" class="panel-body">
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||
bs-tooltip=""
|
||||
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">
|
||||
<i class="fa fa-tasks fa-lg"></i>
|
||||
<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">
|
||||
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<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 class="copy-box" hovering-message="true" value="'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -76,33 +69,43 @@
|
|||
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
|
||||
content-changed="updateForDescription" field-title="'repository description'"></div>
|
||||
|
||||
<!-- Empty message -->
|
||||
<div class="repo-content" ng-show="!currentTag.image_id && !currentImage && !repo.is_building">
|
||||
<div class="empty-message">
|
||||
This repository is empty
|
||||
</div>
|
||||
<!-- Empty messages -->
|
||||
<div ng-if="!currentTag.image_id && !currentImage">
|
||||
<!-- !building && !pushing -->
|
||||
<div class="repo-content" ng-show="!repo.is_building && !isPushing(images)">
|
||||
<div class="empty-message">
|
||||
This repository is empty
|
||||
</div>
|
||||
|
||||
<div class="empty-description" ng-show="repo.can_write">
|
||||
<div class="panel-default">
|
||||
<div class="panel-heading">How to push a new image to this repository:</div>
|
||||
<div class="panel-body">
|
||||
First login to the registry (if you have not done so already):
|
||||
<pre class="command">sudo docker login {{ Config.getDomain() }}</pre>
|
||||
<div class="empty-description" ng-show="repo.can_write">
|
||||
<div class="panel-default">
|
||||
<div class="panel-heading">How to push a new image to this repository:</div>
|
||||
<div class="panel-body">
|
||||
First login to the registry (if you have not done so already):
|
||||
<pre class="command">sudo docker login {{ Config.getDomain() }}</pre>
|
||||
|
||||
Tag an image to this repository:
|
||||
<pre class="command">sudo docker tag <i>0u123imageidgoeshere</i> {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
|
||||
Tag an image to this repository:
|
||||
<pre class="command">sudo docker tag <i>0u123imageidgoeshere</i> {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
|
||||
|
||||
Push the image to this repository:
|
||||
<pre class="command">sudo docker push {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
|
||||
Push the image to this repository:
|
||||
<pre class="command">sudo docker push {{ Config.getDomain() }}/{{repo.namespace}}/{{repo.name}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- building -->
|
||||
<div class="repo-content" ng-show="repo.is_building">
|
||||
<div class="empty-message">
|
||||
A build is currently processing. If this takes longer than an hour, please <a href="/contact">contact us</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-content" ng-show="!currentTag.image_id && repo.is_building">
|
||||
<div class="empty-message">
|
||||
A build is currently processing. If this takes longer than an hour, please <a href="/contact">contact us</a>
|
||||
<!-- pushing -->
|
||||
<div class="repo-content" ng-show="!repo.is_building && isPushing(images)">
|
||||
<div class="empty-message">
|
||||
A push to this repository is in progress.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -357,7 +360,7 @@
|
|||
<div style="margin: 10px; margin-top: 20px;" ng-show="isAnotherImageTag(toTagImage, tagToCreate)">
|
||||
Note: <span class="label tag label-default">{{ tagToCreate }}</span> is already applied to another image. This will <b>move</b> the tag.
|
||||
</div>
|
||||
<div class="tag-specific-images-view" tag="tagToCreate" repository="repo" images="images"
|
||||
<div class="tag-specific-images-view" tag="tagToCreate" repository="repo" images="images" image-cutoff="toTagImage"
|
||||
style="margin: 10px; margin-top: 20px; margin-bottom: -10px;" ng-show="isAnotherImageTag(toTagImage, tagToCreate)">
|
||||
This will also delete any unattach images and delete the following images:
|
||||
</div>
|
||||
|
@ -388,7 +391,10 @@
|
|||
</span>?
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" ng-show="deletingTag">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="!deletingTag">
|
||||
Are you sure you want to delete tag
|
||||
<span class="label tag" ng-class="tagToDelete == currentTag.name ? 'label-success' : 'label-default'">
|
||||
{{ tagToDelete }}
|
||||
|
@ -398,7 +404,7 @@
|
|||
The following images and any other images not referenced by a tag will be deleted:
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer" ng-show="!deletingTag">
|
||||
<button type="button" class="btn btn-primary" ng-click="deleteTag(tagToDelete)">Delete Tag</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
from storage.local import LocalStorage
|
||||
from storage.s3 import S3Storage
|
||||
from storage.cloud import S3Storage, GoogleCloudStorage
|
||||
from storage.fakestorage import FakeStorage
|
||||
from storage.distributedstorage import DistributedStorage
|
||||
|
||||
|
||||
STORAGE_DRIVER_CLASSES = {
|
||||
'LocalStorage': LocalStorage,
|
||||
'S3Storage': S3Storage,
|
||||
'GoogleCloudStorage': GoogleCloudStorage,
|
||||
}
|
||||
|
||||
|
||||
class Storage(object):
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
|
@ -18,13 +25,8 @@ class Storage(object):
|
|||
driver = storage_params[0]
|
||||
parameters = storage_params[1]
|
||||
|
||||
if driver == 'LocalStorage':
|
||||
storage = LocalStorage(**parameters)
|
||||
elif driver == 'S3Storage':
|
||||
storage = S3Storage(**parameters)
|
||||
else:
|
||||
storage = FakeStorage()
|
||||
|
||||
driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage)
|
||||
storage = driver_class(**parameters)
|
||||
storages[location] = storage
|
||||
|
||||
preference = app.config.get('DISTRIBUTED_STORAGE_PREFERENCE', None)
|
||||
|
|
|
@ -3,7 +3,9 @@ import os
|
|||
import logging
|
||||
|
||||
import boto.s3.connection
|
||||
import boto.gs.connection
|
||||
import boto.s3.key
|
||||
import boto.gs.key
|
||||
|
||||
from storage.basestorage import BaseStorage
|
||||
|
||||
|
@ -32,22 +34,24 @@ class StreamReadKeyAsFile(object):
|
|||
return resp
|
||||
|
||||
|
||||
class S3Storage(BaseStorage):
|
||||
|
||||
def __init__(self, storage_path, s3_access_key, s3_secret_key, s3_bucket):
|
||||
class _CloudStorage(BaseStorage):
|
||||
def __init__(self, connection_class, key_class, upload_params, storage_path, access_key,
|
||||
secret_key, bucket_name):
|
||||
self._initialized = False
|
||||
self._bucket_name = s3_bucket
|
||||
self._access_key = s3_access_key
|
||||
self._secret_key = s3_secret_key
|
||||
self._bucket_name = bucket_name
|
||||
self._access_key = access_key
|
||||
self._secret_key = secret_key
|
||||
self._root_path = storage_path
|
||||
self._s3_conn = None
|
||||
self._s3_bucket = None
|
||||
self._connection_class = connection_class
|
||||
self._key_class = key_class
|
||||
self._upload_params = upload_params
|
||||
self._cloud_conn = None
|
||||
self._cloud_bucket = None
|
||||
|
||||
def _initialize_s3(self):
|
||||
def _initialize_cloud_conn(self):
|
||||
if not self._initialized:
|
||||
self._s3_conn = boto.s3.connection.S3Connection(self._access_key,
|
||||
self._secret_key)
|
||||
self._s3_bucket = self._s3_conn.get_bucket(self._bucket_name)
|
||||
self._cloud_conn = self._connection_class(self._access_key, self._secret_key)
|
||||
self._cloud_bucket = self._cloud_conn.get_bucket(self._bucket_name)
|
||||
self._initialized = True
|
||||
|
||||
def _debug_key(self, key):
|
||||
|
@ -69,33 +73,33 @@ class S3Storage(BaseStorage):
|
|||
return path
|
||||
|
||||
def get_content(self, path):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
if not key.exists():
|
||||
raise IOError('No such key: \'{0}\''.format(path))
|
||||
return key.get_contents_as_string()
|
||||
|
||||
def put_content(self, path, content):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key.set_contents_from_string(content, encrypt_key=True)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
key.set_contents_from_string(content, **self._upload_params)
|
||||
return path
|
||||
|
||||
def get_supports_resumeable_downloads(self):
|
||||
return True
|
||||
|
||||
def get_direct_download_url(self, path, expires_in=60):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
k = boto.s3.key.Key(self._s3_bucket, path)
|
||||
k = self._key_class(self._cloud_bucket, path)
|
||||
return k.generate_url(expires_in)
|
||||
|
||||
def stream_read(self, path):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
if not key.exists():
|
||||
raise IOError('No such key: \'{0}\''.format(path))
|
||||
while True:
|
||||
|
@ -105,21 +109,21 @@ class S3Storage(BaseStorage):
|
|||
yield buf
|
||||
|
||||
def stream_read_file(self, path):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
if not key.exists():
|
||||
raise IOError('No such key: \'{0}\''.format(path))
|
||||
return StreamReadKeyAsFile(key)
|
||||
|
||||
def stream_write(self, path, fp):
|
||||
# Minimum size of upload part size on S3 is 5MB
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
buffer_size = 5 * 1024 * 1024
|
||||
if self.buffer_size > buffer_size:
|
||||
buffer_size = self.buffer_size
|
||||
path = self._init_path(path)
|
||||
mp = self._s3_bucket.initiate_multipart_upload(path, encrypt_key=True)
|
||||
mp = self._cloud_bucket.initiate_multipart_upload(path, **self._upload_params)
|
||||
num_part = 1
|
||||
while True:
|
||||
try:
|
||||
|
@ -135,7 +139,7 @@ class S3Storage(BaseStorage):
|
|||
mp.complete_upload()
|
||||
|
||||
def list_directory(self, path=None):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
|
@ -143,7 +147,7 @@ class S3Storage(BaseStorage):
|
|||
if self._root_path != '/':
|
||||
ln = len(self._root_path)
|
||||
exists = False
|
||||
for key in self._s3_bucket.list(prefix=path, delimiter='/'):
|
||||
for key in self._cloud_bucket.list(prefix=path, delimiter='/'):
|
||||
exists = True
|
||||
name = key.name
|
||||
if name.endswith('/'):
|
||||
|
@ -156,15 +160,15 @@ class S3Storage(BaseStorage):
|
|||
raise OSError('No such directory: \'{0}\''.format(path))
|
||||
|
||||
def exists(self, path):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
return key.exists()
|
||||
|
||||
def remove(self, path):
|
||||
self._initialize_s3()
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = boto.s3.key.Key(self._s3_bucket, path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
if key.exists():
|
||||
# It's a file
|
||||
key.delete()
|
||||
|
@ -172,5 +176,28 @@ class S3Storage(BaseStorage):
|
|||
# We assume it's a directory
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
for key in self._s3_bucket.list(prefix=path):
|
||||
for key in self._cloud_bucket.list(prefix=path):
|
||||
key.delete()
|
||||
|
||||
|
||||
class S3Storage(_CloudStorage):
|
||||
def __init__(self, storage_path, s3_access_key, s3_secret_key, s3_bucket):
|
||||
upload_params = {
|
||||
'encrypt_key': True,
|
||||
}
|
||||
super(S3Storage, self).__init__(boto.s3.connection.S3Connection, boto.s3.key.Key,
|
||||
upload_params, storage_path, s3_access_key, s3_secret_key,
|
||||
s3_bucket)
|
||||
|
||||
|
||||
class GoogleCloudStorage(_CloudStorage):
|
||||
def __init__(self, storage_path, access_key, secret_key, bucket_name):
|
||||
super(GoogleCloudStorage, self).__init__(boto.gs.connection.GSConnection, boto.gs.key.Key, {},
|
||||
storage_path, access_key, secret_key, bucket_name)
|
||||
|
||||
def stream_write(self, path, fp):
|
||||
# Minimum size of upload part size on S3 is 5MB
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
key = self._key_class(self._cloud_bucket, path)
|
||||
key.set_contents_from_stream(fp)
|
|
@ -91,42 +91,40 @@ mixpanel.init("{{ mixpanel_key }}", { track_pageview : false, debug: {{ is_debug
|
|||
{% endif %}
|
||||
|
||||
</head>
|
||||
<body ng-class="pageClass">
|
||||
<div ng-class="!fixFooter ? 'wrapper' : ''">
|
||||
<nav class="navbar navbar-default header-bar" role="navigation"></nav>
|
||||
<body ng-class="pageClass + ' ' + (user.anonymous ? 'anon' : 'signedin')" class="co-img-bg-network">
|
||||
<div id="co-l-footer-wrapper">
|
||||
<nav class="navbar navbar-default header-bar co-m-navbar co-fx-box-shadow" role="navigation"></nav>
|
||||
|
||||
{% block body_content %}
|
||||
<div id="padding-container">
|
||||
<div id="co-l-view-container">
|
||||
<div class="main-panel co-fx-box-shadow-heavy">
|
||||
{% block body_content %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<div ng-class="!fixFooter ? 'push' : ''"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer-container" ng-class="fixFooter ? 'fixed' : ''">
|
||||
<div class="page-footer-padder">
|
||||
<nav class="page-footer hidden-xs">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<ul>
|
||||
<li><span class="copyright">©2014 DevTable, LLC</span></li>
|
||||
<li quay-require="['BILLING']"><a href="http://blog.devtable.com/" target="_blank">Blog</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/tos" target="_self">Terms</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/privacy" target="_self">Privacy</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/security/" target="_self">Security</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/about/" target="_self">About</a></li>
|
||||
<li><b><a href="/contact/" target="_self">Contact</a></b></li>
|
||||
<li quay-require="['BILLING']"><b><a href="http://status.quay.io" target="_self">Service Status</a></b></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 logo-container">
|
||||
<a href="https://devtable.com"><img class="dt-logo" src="/static/img/dt-logo.png"></a>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</nav>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="co-l-footer-push"></div>
|
||||
</div>
|
||||
|
||||
<nav id="co-l-footer" class="page-footer hidden-xs">
|
||||
<div class="col-md-8">
|
||||
<ul>
|
||||
<li><span class="copyright">©2014 CoreOS, Inc.</span></li>
|
||||
<li quay-require="['BILLING']"><a href="/tos" target="_self">Terms</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/privacy" target="_self">Privacy</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/security/" target="_self">Security</a></li>
|
||||
<li quay-require="['BILLING']"><a href="/about/" target="_self">About</a></li>
|
||||
<li><b><a href="/contact/" target="_self">Contact</a></b></li>
|
||||
<li quay-require="['BILLING']"><b><a href="http://status.quay.io" target="_self">Service Status</a></b></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<a href="https://coreos.com" target="_blank"><img src="/static/img/coreos.svg"></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="couldnotloadModal" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
{% block body_content %}
|
||||
<!-- Note: Must be in the <body> tag -->
|
||||
<script src="static/standalonelib/jquery.overscroll.min.js"></script>
|
||||
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
|
@ -35,23 +35,18 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
{% if not has_billing %}
|
||||
<!-- 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-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Cannot create user</h4>
|
||||
<h4 class="modal-title">Cannot Contact External Service</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A new user cannot be created as this organization has reached its licensed seat count. Please contact your administrator.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-dismiss="modal" onclick="location = '/signin'">Sign In</a>
|
||||
A connection to an external service has failed. Please reload the page to try again.
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -196,7 +196,7 @@ def build_index_specs():
|
|||
IndexTestSpec(url_for('index.put_repository_auth', repository=PUBLIC_REPO),
|
||||
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),
|
||||
|
||||
|
|
|
@ -14,14 +14,17 @@ from endpoints.api.search import FindRepositories, EntitySearch
|
|||
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||
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,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification)
|
||||
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification,
|
||||
VerifyUser)
|
||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
||||
|
@ -37,7 +40,7 @@ from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repos
|
|||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||
|
||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
||||
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||
|
||||
|
||||
try:
|
||||
|
@ -432,6 +435,24 @@ class TestSignin(ApiTestCase):
|
|||
self._run_test('POST', 403, 'devtable', {u'username': 'E9RY', u'password': 'LQ0N'})
|
||||
|
||||
|
||||
class TestVerifyUser(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(VerifyUser)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, {u'password': 'LQ0N'})
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', {u'password': 'LQ0N'})
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', {u'password': 'LQ0N'})
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 200, 'devtable', {u'password': 'password'})
|
||||
|
||||
|
||||
class TestListPlans(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
@ -471,13 +492,13 @@ class TestUser(ApiTestCase):
|
|||
self._run_test('PUT', 401, None, {})
|
||||
|
||||
def test_put_freshuser(self):
|
||||
self._run_test('PUT', 200, 'freshuser', {})
|
||||
self._run_test('PUT', 401, 'freshuser', {})
|
||||
|
||||
def test_put_reader(self):
|
||||
self._run_test('PUT', 200, 'reader', {})
|
||||
self._run_test('PUT', 401, 'reader', {})
|
||||
|
||||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 200, 'devtable', {})
|
||||
self._run_test('PUT', 401, 'devtable', {})
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 400, None, {u'username': 'T946', u'password': '0SG4', u'email': 'MENT'})
|
||||
|
@ -1632,6 +1653,19 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
|||
ApiTestCase.setUp(self)
|
||||
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):
|
||||
self._run_test('PUT', 401, None, None)
|
||||
|
||||
|
@ -1644,6 +1678,7 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
|||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 400, 'devtable', None)
|
||||
|
||||
|
||||
def test_delete_anonymous(self):
|
||||
self._run_test('DELETE', 401, None, None)
|
||||
|
||||
|
@ -3040,6 +3075,19 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
ApiTestCase.setUp(self)
|
||||
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):
|
||||
self._run_test('PUT', 401, None, None)
|
||||
|
||||
|
@ -3052,6 +3100,7 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 201, 'devtable', None)
|
||||
|
||||
|
||||
def test_delete_anonymous(self):
|
||||
self._run_test('DELETE', 401, None, None)
|
||||
|
||||
|
@ -3065,6 +3114,42 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
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):
|
||||
def 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.image import RepositoryImage, RepositoryImageList
|
||||
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,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
|
@ -40,7 +41,7 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember,
|
|||
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
|
||||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
||||
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||
|
||||
try:
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
@ -326,7 +327,13 @@ class TestChangeUserDetails(ApiTestCase):
|
|||
self.putJsonResponse(User,
|
||||
data=dict(password='newpasswordiscool'))
|
||||
self.login(READ_ACCESS_USER, password='newpasswordiscool')
|
||||
|
||||
|
||||
def test_changeeemail(self):
|
||||
self.login(READ_ACCESS_USER)
|
||||
|
||||
self.putJsonResponse(User,
|
||||
data=dict(email='test+foo@devtable.com'))
|
||||
|
||||
def test_changeinvoiceemail(self):
|
||||
self.login(READ_ACCESS_USER)
|
||||
|
||||
|
@ -1572,6 +1579,30 @@ class TestUserRobots(ApiTestCase):
|
|||
robots = self.getRobotNames()
|
||||
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):
|
||||
def getRobotNames(self):
|
||||
|
@ -1601,6 +1632,31 @@ class TestOrgRobots(ApiTestCase):
|
|||
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):
|
||||
def test_user_logs(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
|
|
@ -46,25 +46,30 @@ class TestImageSharing(unittest.TestCase):
|
|||
preferred = storage.preferred_locations[0]
|
||||
image = model.find_create_or_link_image(docker_image_id, repository_obj, username, {},
|
||||
preferred)
|
||||
return image.storage.id
|
||||
image.storage.uploading = False
|
||||
image.storage.save()
|
||||
return image.storage
|
||||
|
||||
def assertSameStorage(self, docker_image_id, storage_id, repository=REPO, username=ADMIN_ACCESS_USER):
|
||||
new_storage_id = self.createStorage(docker_image_id, repository, username)
|
||||
self.assertEquals(storage_id, new_storage_id)
|
||||
def assertSameStorage(self, docker_image_id, existing_storage, repository=REPO,
|
||||
username=ADMIN_ACCESS_USER):
|
||||
new_storage = self.createStorage(docker_image_id, repository, username)
|
||||
self.assertEquals(existing_storage.id, new_storage.id)
|
||||
|
||||
def assertDifferentStorage(self, docker_image_id, storage_id, repository=REPO, username=ADMIN_ACCESS_USER):
|
||||
new_storage_id = self.createStorage(docker_image_id, repository, username)
|
||||
self.assertNotEquals(storage_id, new_storage_id)
|
||||
def assertDifferentStorage(self, docker_image_id, existing_storage, repository=REPO,
|
||||
username=ADMIN_ACCESS_USER):
|
||||
new_storage = self.createStorage(docker_image_id, repository, username)
|
||||
self.assertNotEquals(existing_storage.id, new_storage.id)
|
||||
|
||||
|
||||
def test_same_user(self):
|
||||
""" The same user creates two images, each which should be shared in the same repo. This is a sanity check. """
|
||||
""" The same user creates two images, each which should be shared in the same repo. This is a
|
||||
sanity check. """
|
||||
|
||||
# Create a reference to a new docker ID => new image.
|
||||
first_storage_id = self.createStorage('first-image')
|
||||
first_storage = self.createStorage('first-image')
|
||||
|
||||
# Create a reference to the same docker ID => same image.
|
||||
self.assertSameStorage('first-image', first_storage_id)
|
||||
self.assertSameStorage('first-image', first_storage)
|
||||
|
||||
# Create a reference to another new docker ID => new image.
|
||||
second_storage_id = self.createStorage('second-image')
|
||||
|
@ -73,68 +78,68 @@ class TestImageSharing(unittest.TestCase):
|
|||
self.assertSameStorage('second-image', second_storage_id)
|
||||
|
||||
# Make sure the images are different.
|
||||
self.assertNotEquals(first_storage_id, second_storage_id)
|
||||
self.assertNotEquals(first_storage, second_storage_id)
|
||||
|
||||
|
||||
def test_no_user_private_repo(self):
|
||||
""" If no user is specified (token case usually), then no sharing can occur on a private repo. """
|
||||
# Create a reference to a new docker ID => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=None, repository=SHARED_REPO)
|
||||
first_storage = self.createStorage('the-image', username=None, repository=SHARED_REPO)
|
||||
|
||||
# Create a areference to the same docker ID, but since no username => new image.
|
||||
self.assertDifferentStorage('the-image', first_storage_id, username=None, repository=RANDOM_REPO)
|
||||
self.assertDifferentStorage('the-image', first_storage, username=None, repository=RANDOM_REPO)
|
||||
|
||||
|
||||
def test_no_user_public_repo(self):
|
||||
""" If no user is specified (token case usually), then no sharing can occur on a private repo except when the image is first public. """
|
||||
# Create a reference to a new docker ID => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=None, repository=PUBLIC_REPO)
|
||||
first_storage = self.createStorage('the-image', username=None, repository=PUBLIC_REPO)
|
||||
|
||||
# Create a areference to the same docker ID. Since no username, we'd expect different but the first image is public so => shaed image.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=None, repository=RANDOM_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=None, repository=RANDOM_REPO)
|
||||
|
||||
|
||||
def test_different_user_same_repo(self):
|
||||
""" Two different users create the same image in the same repo. """
|
||||
|
||||
# Create a reference to a new docker ID under the first user => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=PUBLIC_USER, repository=SHARED_REPO)
|
||||
first_storage = self.createStorage('the-image', username=PUBLIC_USER, repository=SHARED_REPO)
|
||||
|
||||
# Create a reference to the *same* docker ID under the second user => same image.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=ADMIN_ACCESS_USER, repository=SHARED_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=ADMIN_ACCESS_USER, repository=SHARED_REPO)
|
||||
|
||||
|
||||
def test_different_repo_no_shared_access(self):
|
||||
""" Neither user has access to the other user's repository. """
|
||||
|
||||
# Create a reference to a new docker ID under the first user => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=RANDOM_USER, repository=RANDOM_REPO)
|
||||
first_storage = self.createStorage('the-image', username=RANDOM_USER, repository=RANDOM_REPO)
|
||||
|
||||
# Create a reference to the *same* docker ID under the second user => new image.
|
||||
second_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
|
||||
# Verify that the users do not share storage.
|
||||
self.assertNotEquals(first_storage_id, second_storage_id)
|
||||
self.assertNotEquals(first_storage, second_storage_id)
|
||||
|
||||
|
||||
def test_public_than_private(self):
|
||||
""" An image is created publicly then used privately, so it should be shared. """
|
||||
|
||||
# Create a reference to a new docker ID under the first user => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
first_storage = self.createStorage('the-image', username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
|
||||
# Create a reference to the *same* docker ID under the second user => same image, since the first was public.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
|
||||
|
||||
def test_private_than_public(self):
|
||||
""" An image is created privately then used publicly, so it should *not* be shared. """
|
||||
|
||||
# Create a reference to a new docker ID under the first user => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
|
||||
# Create a reference to the *same* docker ID under the second user => new image, since the first was private.
|
||||
self.assertDifferentStorage('the-image', first_storage_id, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
self.assertDifferentStorage('the-image', first_storage, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
|
||||
|
||||
def test_different_repo_with_access(self):
|
||||
|
@ -143,64 +148,71 @@ class TestImageSharing(unittest.TestCase):
|
|||
be shared since the user has access.
|
||||
"""
|
||||
# Create the image in the shared repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=SHARED_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=SHARED_REPO)
|
||||
|
||||
# Create the image in the other user's repo, but since the user (PUBLIC) still has access to the shared
|
||||
# repository, they should reuse the storage.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
|
||||
|
||||
def test_org_access(self):
|
||||
""" An image is accessible by being a member of the organization. """
|
||||
|
||||
# Create the new image under the org's repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
|
||||
# Create an image under the user's repo, but since the user has access to the organization => shared image.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=ADMIN_ACCESS_USER, repository=REPO)
|
||||
|
||||
# Ensure that the user's robot does not have access, since it is not on the permissions list for the repo.
|
||||
self.assertDifferentStorage('the-image', first_storage_id, username=ADMIN_ROBOT_USER, repository=SHARED_REPO)
|
||||
self.assertDifferentStorage('the-image', first_storage, username=ADMIN_ROBOT_USER, repository=SHARED_REPO)
|
||||
|
||||
|
||||
def test_org_access_different_user(self):
|
||||
""" An image is accessible by being a member of the organization. """
|
||||
|
||||
# Create the new image under the org's repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
|
||||
# Create an image under a user's repo, but since the user has access to the organization => shared image.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=PUBLIC_USER, repository=PUBLIC_REPO)
|
||||
|
||||
# Also verify for reader.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=READ_ACCESS_USER, repository=PUBLIC_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=READ_ACCESS_USER, repository=PUBLIC_REPO)
|
||||
|
||||
|
||||
def test_org_no_access(self):
|
||||
""" An image is not accessible if not a member of the organization. """
|
||||
|
||||
# Create the new image under the org's repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
|
||||
# Create an image under a user's repo. Since the user is not a member of the organization => new image.
|
||||
self.assertDifferentStorage('the-image', first_storage_id, username=RANDOM_USER, repository=RANDOM_REPO)
|
||||
self.assertDifferentStorage('the-image', first_storage, username=RANDOM_USER, repository=RANDOM_REPO)
|
||||
|
||||
|
||||
def test_org_not_team_member_with_access(self):
|
||||
""" An image is accessible to a user specifically listed as having permission on the org repo. """
|
||||
|
||||
# Create the new image under the org's repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ORG_REPO)
|
||||
|
||||
# Create an image under a user's repo. Since the user has read access on that repo, they can see the image => shared image.
|
||||
self.assertSameStorage('the-image', first_storage_id, username=OUTSIDE_ORG_USER, repository=OUTSIDE_ORG_REPO)
|
||||
self.assertSameStorage('the-image', first_storage, username=OUTSIDE_ORG_USER, repository=OUTSIDE_ORG_REPO)
|
||||
|
||||
|
||||
def test_org_not_team_member_with_no_access(self):
|
||||
""" A user that has access to one org repo but not another and is not a team member. """
|
||||
|
||||
# Create the new image under the org's repo => new image.
|
||||
first_storage_id = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ANOTHER_ORG_REPO)
|
||||
first_storage = self.createStorage('the-image', username=ADMIN_ACCESS_USER, repository=ANOTHER_ORG_REPO)
|
||||
|
||||
# Create an image under a user's repo. The user doesn't have access to the repo (ANOTHER_ORG_REPO) so => new image.
|
||||
self.assertDifferentStorage('the-image', first_storage_id, username=OUTSIDE_ORG_USER, repository=OUTSIDE_ORG_REPO)
|
||||
self.assertDifferentStorage('the-image', first_storage, username=OUTSIDE_ORG_USER, repository=OUTSIDE_ORG_REPO)
|
||||
|
||||
def test_no_link_to_uploading(self):
|
||||
still_uploading = self.createStorage('an-image', repository=PUBLIC_REPO)
|
||||
still_uploading.uploading = True
|
||||
still_uploading.save()
|
||||
|
||||
self.assertDifferentStorage('an-image', still_uploading)
|
||||
|
|
|
@ -20,7 +20,7 @@ query = (Image
|
|||
.join(ImageStorage)
|
||||
.switch(Image)
|
||||
.join(Repository)
|
||||
.where(Repository.name == 'userportal', Repository.namespace == 'crsinc'))
|
||||
.where(ImageStorage.uploading == False))
|
||||
|
||||
bad_count = 0
|
||||
good_count = 0
|
||||
|
|
|
@ -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 util.invoice import renderInvoiceToHtml
|
||||
|
|
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
|
@ -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
|
@ -0,0 +1,4 @@
|
|||
#! /bin/bash
|
||||
|
||||
service apache2 start
|
||||
mysqld
|
|
@ -1,4 +1,3 @@
|
|||
from app import stripe
|
||||
from app import app
|
||||
|
||||
from util.useremails import send_confirmation_email
|
||||
|
|
|
@ -30,7 +30,11 @@ class SendToMixpanel(Process):
|
|||
while True:
|
||||
mp_request = self._mp_queue.get()
|
||||
logger.debug('Got queued mixpanel reqeust.')
|
||||
self._consumer.send(*json.loads(mp_request))
|
||||
try:
|
||||
self._consumer.send(*json.loads(mp_request))
|
||||
except:
|
||||
# Make sure we don't crash if Mixpanel request fails.
|
||||
pass
|
||||
|
||||
|
||||
class FakeMixpanel(object):
|
||||
|
|
5
util/backoff.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
def exponential_backoff(attempts, scaling_factor, base):
|
||||
backoff = 5 * (pow(2, attempts) - 1)
|
||||
backoff_time = backoff * scaling_factor
|
||||
retry_at = backoff_time/10 + base
|
||||
return retry_at
|
|
@ -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()
|