Merge remote-tracking branch 'origin/master' into rustedbuilds
This commit is contained in:
commit
fc4983ed8b
75 changed files with 4280 additions and 700 deletions
|
@ -36,7 +36,6 @@ start the workers:
|
|||
|
||||
```
|
||||
STACK=prod python -m workers.diffsworker -D
|
||||
STACK=prod python -m workers.dockerfilebuild -D
|
||||
STACK=prod python -m workers.webhookworker -D
|
||||
```
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
|||
from app import app as application
|
||||
from data.model import db as model_db
|
||||
|
||||
|
||||
# Initialize logging
|
||||
application.config['LOGGING_CONFIG']()
|
||||
|
||||
|
@ -16,6 +15,7 @@ from endpoints.web import web
|
|||
from endpoints.tags import tags
|
||||
from endpoints.registry import registry
|
||||
from endpoints.webhooks import webhooks
|
||||
from endpoints.realtime import realtime
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -26,6 +26,7 @@ application.register_blueprint(tags, url_prefix='/v1')
|
|||
application.register_blueprint(registry, url_prefix='/v1')
|
||||
application.register_blueprint(api, url_prefix='/api')
|
||||
application.register_blueprint(webhooks, url_prefix='/webhooks')
|
||||
application.register_blueprint(realtime, url_prefix='/realtime')
|
||||
|
||||
|
||||
def close_db(exc):
|
||||
|
|
|
@ -29,7 +29,7 @@ def process_basic_auth(auth):
|
|||
logger.debug('Invalid basic auth format.')
|
||||
return
|
||||
|
||||
credentials = b64decode(normalized[1]).split(':')
|
||||
credentials = b64decode(normalized[1]).split(':', 1)
|
||||
|
||||
if len(credentials) != 2:
|
||||
logger.debug('Invalid basic auth credential format.')
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
binary_dependencies/builder/nsexec_1.22ubuntu1trusty1_amd64.deb
Normal file
BIN
binary_dependencies/builder/nsexec_1.22ubuntu1trusty1_amd64.deb
Normal file
Binary file not shown.
17
config.py
17
config.py
|
@ -6,6 +6,7 @@ from storage.s3 import S3Storage
|
|||
from storage.local import LocalStorage
|
||||
from data.userfiles import UserRequestFiles
|
||||
from data.buildlogs import BuildLogs
|
||||
from data.userevent import UserEventBuilder
|
||||
from util import analytics
|
||||
|
||||
from test.teststorage import FakeStorage, FakeUserfiles
|
||||
|
@ -15,7 +16,7 @@ from test.testlogs import TestBuildLogs
|
|||
|
||||
class FlaskConfig(object):
|
||||
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
||||
|
||||
JSONIFY_PRETTYPRINT_REGULAR = False
|
||||
|
||||
class FlaskProdConfig(FlaskConfig):
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
@ -91,6 +92,10 @@ class RedisBuildLogs(object):
|
|||
BUILDLOGS = BuildLogs('logs.quay.io')
|
||||
|
||||
|
||||
class UserEventConfig(object):
|
||||
USER_EVENTS = UserEventBuilder('logs.quay.io')
|
||||
|
||||
|
||||
class TestBuildLogs(object):
|
||||
BUILDLOGS = TestBuildLogs('logs.quay.io', 'devtable', 'building',
|
||||
'deadbeef-dead-beef-dead-beefdeadbeef')
|
||||
|
@ -159,7 +164,8 @@ def logs_init_builder(level=logging.DEBUG,
|
|||
|
||||
|
||||
class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
|
||||
FakeAnalytics, StripeTestConfig, RedisBuildLogs):
|
||||
FakeAnalytics, StripeTestConfig, RedisBuildLogs,
|
||||
UserEventConfig):
|
||||
LOGGING_CONFIG = logs_init_builder(logging.WARN)
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
TESTING = True
|
||||
|
@ -168,7 +174,7 @@ class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
|
|||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
||||
DigitalOceanConfig, BuildNodeConfig, S3Userfiles,
|
||||
TestBuildLogs):
|
||||
UserEventConfig, TestBuildLogs):
|
||||
LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter())
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
|
@ -177,7 +183,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
|||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelTestConfig,
|
||||
GitHubProdConfig, DigitalOceanConfig,
|
||||
BuildNodeConfig, S3Userfiles, RedisBuildLogs):
|
||||
BuildNodeConfig, S3Userfiles, RedisBuildLogs,
|
||||
UserEventConfig):
|
||||
LOGGING_CONFIG = logs_init_builder()
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
|
||||
|
@ -185,7 +192,7 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
|||
class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelProdConfig,
|
||||
GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig,
|
||||
S3Userfiles, RedisBuildLogs):
|
||||
S3Userfiles, RedisBuildLogs, UserEventConfig):
|
||||
|
||||
LOGGING_CONFIG = logs_init_builder()
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
|
|
|
@ -3,6 +3,10 @@ import json
|
|||
|
||||
|
||||
class BuildLogs(object):
|
||||
ERROR = 'error'
|
||||
COMMAND = 'command'
|
||||
PHASE = 'phase'
|
||||
|
||||
def __init__(self, redis_host):
|
||||
self._redis = redis.StrictRedis(host=redis_host)
|
||||
|
||||
|
@ -10,10 +14,6 @@ class BuildLogs(object):
|
|||
def _logs_key(build_id):
|
||||
return 'builds/%s/logs' % build_id
|
||||
|
||||
@staticmethod
|
||||
def _commands_key(build_id):
|
||||
return 'builds/%s/commands' % build_id
|
||||
|
||||
def append_log_entry(self, build_id, log_obj):
|
||||
"""
|
||||
Appends the serialized form of log_obj to the end of the log entry list
|
||||
|
@ -21,7 +21,7 @@ class BuildLogs(object):
|
|||
"""
|
||||
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj))
|
||||
|
||||
def append_log_message(self, build_id, log_message):
|
||||
def append_log_message(self, build_id, log_message, log_type=None):
|
||||
"""
|
||||
Wraps the message in an envelope and push it to the end of the log entry
|
||||
list and returns the index at which it was inserted.
|
||||
|
@ -29,54 +29,21 @@ class BuildLogs(object):
|
|||
log_obj = {
|
||||
'message': log_message
|
||||
}
|
||||
|
||||
if log_type:
|
||||
log_obj['type'] = log_type
|
||||
|
||||
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
|
||||
|
||||
def append_command_message(self, build_id, command_message):
|
||||
"""
|
||||
Wraps the message in an envelope and push it to the end of the log entry
|
||||
list, to the commands list, and returns the new length of the list.
|
||||
"""
|
||||
log_obj = {
|
||||
'message': command_message,
|
||||
'is_command': True,
|
||||
}
|
||||
idx = self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
|
||||
|
||||
cmd_obj = {
|
||||
'message': command_message,
|
||||
'index': idx,
|
||||
}
|
||||
self._redis.rpush(self._commands_key(build_id), json.dumps(cmd_obj))
|
||||
return idx
|
||||
|
||||
def get_log_entries(self, build_id, start_index, end_index):
|
||||
def get_log_entries(self, build_id, start_index):
|
||||
"""
|
||||
Returns a tuple of the current length of the list and an iterable of the
|
||||
requested log entries. End index is inclusive.
|
||||
requested log entries.
|
||||
"""
|
||||
llen = self._redis.llen(self._logs_key(build_id))
|
||||
log_entries = self._redis.lrange(self._logs_key(build_id), start_index,
|
||||
end_index)
|
||||
log_entries = self._redis.lrange(self._logs_key(build_id), start_index, -1)
|
||||
return (llen, (json.loads(entry) for entry in log_entries))
|
||||
|
||||
def get_commands(self, build_id):
|
||||
"""
|
||||
Returns a list of all Dockerfile commands that have passed through the
|
||||
specified build thus far.
|
||||
"""
|
||||
commands = self._redis.lrange(self._commands_key(build_id), 0, -1)
|
||||
return (json.loads(cmd) for cmd in commands)
|
||||
|
||||
def get_last_command(self, build_id):
|
||||
"""
|
||||
Returns only the last command from the list of commands.
|
||||
"""
|
||||
commands = self._redis.lrange(self._commands_key(build_id), -1, -1)
|
||||
if commands:
|
||||
return json.loads(commands[-1])
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _status_key(build_id):
|
||||
return 'builds/%s/status' % build_id
|
||||
|
|
|
@ -216,7 +216,7 @@ class RepositoryBuild(BaseModel):
|
|||
tag = CharField()
|
||||
phase = CharField(default='waiting')
|
||||
started = DateTimeField(default=datetime.now)
|
||||
|
||||
display_name = CharField()
|
||||
|
||||
|
||||
class QueueItem(BaseModel):
|
||||
|
|
|
@ -109,7 +109,10 @@ def create_organization(name, email, creating_user):
|
|||
|
||||
return new_org
|
||||
except InvalidUsernameException:
|
||||
raise InvalidOrganizationException('Invalid organization name: %s' % name)
|
||||
msg = ('Invalid organization name: %s Organization names must consist ' +
|
||||
'solely of lower case letters, numbers, and underscores. ' +
|
||||
'[a-z0-9_]') % name
|
||||
raise InvalidOrganizationException(msg)
|
||||
|
||||
|
||||
def create_robot(robot_shortname, parent):
|
||||
|
@ -760,6 +763,16 @@ def get_all_repo_users(namespace_name, repository_name):
|
|||
Repository.name == repository_name)
|
||||
|
||||
|
||||
def get_repository_for_resource(resource_key):
|
||||
joined = Repository.select().join(RepositoryBuild)
|
||||
query = joined.where(RepositoryBuild.resource_key == resource_key).limit(1)
|
||||
result = list(query)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_repository(namespace_name, repository_name):
|
||||
try:
|
||||
return Repository.get(Repository.name == repository_name,
|
||||
|
@ -970,6 +983,41 @@ def delete_tag_and_images(namespace_name, repository_name, tag_name):
|
|||
store.remove(repository_path)
|
||||
|
||||
|
||||
def garbage_collect_repository(namespace_name, repository_name):
|
||||
# Get a list of all images used by tags in the repository
|
||||
tag_query = (RepositoryTag
|
||||
.select(RepositoryTag, Image)
|
||||
.join(Image)
|
||||
.switch(RepositoryTag)
|
||||
.join(Repository)
|
||||
.where(Repository.name == repository_name,
|
||||
Repository.namespace == namespace_name))
|
||||
|
||||
referenced_anscestors = set()
|
||||
for tag in tag_query:
|
||||
# The anscestor list is in the format '/1/2/3/', extract just the ids
|
||||
anscestor_id_strings = tag.image.ancestors.split('/')[1:-1]
|
||||
ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings]
|
||||
referenced_anscestors = referenced_anscestors.union(set(ancestor_list))
|
||||
referenced_anscestors.add(tag.image.id)
|
||||
|
||||
all_repo_images = get_repository_images(namespace_name, repository_name)
|
||||
all_images = {int(img.id):img for img in all_repo_images}
|
||||
to_remove = set(all_images.keys()).difference(referenced_anscestors)
|
||||
|
||||
logger.info('Cleaning up unreferenced images: %s', to_remove)
|
||||
|
||||
for image_id_to_remove in to_remove:
|
||||
image_to_remove = all_images[image_id_to_remove]
|
||||
image_path = store.image_path(namespace_name, repository_name,
|
||||
image_to_remove.docker_image_id)
|
||||
image_to_remove.delete_instance()
|
||||
logger.debug('Deleting image storage: %s' % image_path)
|
||||
store.remove(image_path)
|
||||
|
||||
return len(to_remove)
|
||||
|
||||
|
||||
def get_tag_image(namespace_name, repository_name, tag_name):
|
||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||
fetched = list(joined.where(Repository.name == repository_name,
|
||||
|
@ -1021,7 +1069,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
|||
(namespace_name, repository_name))
|
||||
|
||||
try:
|
||||
image = Image.get(Image.docker_image_id == tag_docker_image_id)
|
||||
image = Image.get(Image.docker_image_id == tag_docker_image_id,
|
||||
Image.repository == repo)
|
||||
except Image.DoesNotExist:
|
||||
raise DataModelException('Invalid image with id: %s' %
|
||||
tag_docker_image_id)
|
||||
|
@ -1270,9 +1319,11 @@ def list_repository_builds(namespace_name, repository_name,
|
|||
return fetched
|
||||
|
||||
|
||||
def create_repository_build(repo, access_token, resource_key, tag):
|
||||
def create_repository_build(repo, access_token, resource_key, tag,
|
||||
display_name):
|
||||
return RepositoryBuild.create(repository=repo, access_token=access_token,
|
||||
resource_key=resource_key, tag=tag)
|
||||
resource_key=resource_key, tag=tag,
|
||||
display_name=display_name)
|
||||
|
||||
|
||||
def create_webhook(repo, params_obj):
|
||||
|
|
|
@ -64,5 +64,5 @@ class WorkQueue(object):
|
|||
|
||||
|
||||
image_diff_queue = WorkQueue('imagediff')
|
||||
dockerfile_build_queue = WorkQueue('dockerfilebuild')
|
||||
dockerfile_build_queue = WorkQueue('dockerfilebuild2')
|
||||
webhook_queue = WorkQueue('webhook')
|
||||
|
|
90
data/userevent.py
Normal file
90
data/userevent.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
import redis
|
||||
import json
|
||||
import threading
|
||||
|
||||
class UserEventBuilder(object):
|
||||
"""
|
||||
Defines a helper class for constructing UserEvent and UserEventListener
|
||||
instances.
|
||||
"""
|
||||
def __init__(self, redis_host):
|
||||
self._redis_host = redis_host
|
||||
|
||||
def get_event(self, username):
|
||||
return UserEvent(self._redis_host, username)
|
||||
|
||||
def get_listener(self, username, events):
|
||||
return UserEventListener(self._redis_host, username, events)
|
||||
|
||||
|
||||
class UserEvent(object):
|
||||
"""
|
||||
Defines a helper class for publishing to realtime user events
|
||||
as backed by Redis.
|
||||
"""
|
||||
def __init__(self, redis_host, username):
|
||||
self._redis = redis.StrictRedis(host=redis_host)
|
||||
self._username = username
|
||||
|
||||
@staticmethod
|
||||
def _user_event_key(username, event_id):
|
||||
return 'user/%s/events/%s' % (username, event_id)
|
||||
|
||||
def publish_event_data_sync(self, event_id, data_obj):
|
||||
return self._redis.publish(self._user_event_key(self._username, event_id), json.dumps(data_obj))
|
||||
|
||||
def publish_event_data(self, event_id, data_obj):
|
||||
"""
|
||||
Publishes the serialized form of the data object for the given event. Note that this occurs
|
||||
in a thread to prevent blocking.
|
||||
"""
|
||||
def conduct():
|
||||
try:
|
||||
self.publish_event_data_sync(event_id, data_obj)
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
thread = threading.Thread(target=conduct)
|
||||
thread.start()
|
||||
|
||||
|
||||
class UserEventListener(object):
|
||||
"""
|
||||
Defines a helper class for subscribing to realtime user events as
|
||||
backed by Redis.
|
||||
"""
|
||||
def __init__(self, redis_host, username, events=set([])):
|
||||
channels = [self._user_event_key(username, e) for e in events]
|
||||
|
||||
self._redis = redis.StrictRedis(host=redis_host)
|
||||
self._pubsub = self._redis.pubsub()
|
||||
self._pubsub.subscribe(channels)
|
||||
|
||||
@staticmethod
|
||||
def _user_event_key(username, event_id):
|
||||
return 'user/%s/events/%s' % (username, event_id)
|
||||
|
||||
def event_stream(self):
|
||||
"""
|
||||
Starts listening for events on the channel(s), yielding for each event
|
||||
found.
|
||||
"""
|
||||
for item in self._pubsub.listen():
|
||||
channel = item['channel']
|
||||
event_id = channel.split('/')[3] # user/{username}/{events}/{id}
|
||||
data = None
|
||||
|
||||
try:
|
||||
data = json.loads(item['data'] or '{}')
|
||||
except:
|
||||
pass
|
||||
|
||||
if data:
|
||||
yield event_id, data
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Unsubscribes from the channel(s). Should be called once the connection
|
||||
has terminated.
|
||||
"""
|
||||
self._pubsub.unsubscribe()
|
|
@ -59,3 +59,9 @@ class UserRequestFiles(object):
|
|||
full_key = os.path.join(self._prefix, file_id)
|
||||
k = Key(self._bucket, full_key)
|
||||
return k.generate_url(expires_in)
|
||||
|
||||
def get_file_checksum(self, file_id):
|
||||
self._initialize_s3()
|
||||
full_key = os.path.join(self._prefix, file_id)
|
||||
k = self._bucket.lookup(full_key)
|
||||
return k.etag[1:-1][:7]
|
||||
|
|
|
@ -262,7 +262,6 @@ def convert_user_to_organization():
|
|||
@internal_api_call
|
||||
def change_user_details():
|
||||
user = current_user.db_user()
|
||||
|
||||
user_data = request.get_json()
|
||||
|
||||
try:
|
||||
|
@ -315,6 +314,8 @@ def create_new_user():
|
|||
@internal_api_call
|
||||
def signin_user():
|
||||
signin_data = request.get_json()
|
||||
if not signin_data:
|
||||
abort(404)
|
||||
|
||||
username = signin_data['username']
|
||||
password = signin_data['password']
|
||||
|
@ -421,6 +422,7 @@ def get_matching_entities(prefix):
|
|||
|
||||
team_data = [entity_team_view(team) for team in teams]
|
||||
user_data = [user_view(user) for user in users]
|
||||
|
||||
return jsonify({
|
||||
'results': team_data + user_data
|
||||
})
|
||||
|
@ -446,11 +448,16 @@ def create_organization():
|
|||
existing = None
|
||||
|
||||
try:
|
||||
existing = (model.get_organization(org_data['name']) or
|
||||
model.get_user(org_data['name']))
|
||||
existing = model.get_organization(org_data['name'])
|
||||
except model.InvalidOrganizationException:
|
||||
pass
|
||||
|
||||
if not existing:
|
||||
try:
|
||||
existing = model.get_user(org_data['name'])
|
||||
except model.InvalidUserException:
|
||||
pass
|
||||
|
||||
if existing:
|
||||
msg = 'A user or organization with this name already exists'
|
||||
return request_error(message=msg)
|
||||
|
@ -604,9 +611,9 @@ def create_organization_prototype_permission(orgname):
|
|||
'name' in details['activating_user']):
|
||||
activating_username = details['activating_user']['name']
|
||||
|
||||
delegate = details['delegate']
|
||||
delegate_kind = delegate['kind']
|
||||
delegate_name = delegate['name']
|
||||
delegate = details['delegate'] if 'delegate' in details else {}
|
||||
delegate_kind = delegate.get('kind', None)
|
||||
delegate_name = delegate.get('name', None)
|
||||
|
||||
delegate_username = delegate_name if delegate_kind == 'user' else None
|
||||
delegate_teamname = delegate_name if delegate_kind == 'team' else None
|
||||
|
@ -622,7 +629,7 @@ def create_organization_prototype_permission(orgname):
|
|||
return request_error(message='Unknown activating user')
|
||||
|
||||
if not delegate_user and not delegate_team:
|
||||
return request_error(message='Missing delagate user or team')
|
||||
return request_error(message='Missing delegate user or team')
|
||||
|
||||
role_name = details['role']
|
||||
|
||||
|
@ -1148,7 +1155,9 @@ def build_status_view(build_obj):
|
|||
'id': build_obj.uuid,
|
||||
'phase': build_obj.phase,
|
||||
'started': build_obj.started,
|
||||
'display_name': build_obj.display_name,
|
||||
'status': status,
|
||||
'resource_key': build_obj.resource_key
|
||||
}
|
||||
|
||||
|
||||
|
@ -1189,38 +1198,12 @@ def get_repo_build_logs(namespace, repository, build_uuid):
|
|||
|
||||
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||
|
||||
start_param = request.args.get('start', None)
|
||||
end = int(request.args.get('end', -1))
|
||||
start = int(request.args.get('start', 0))
|
||||
|
||||
last_command = None
|
||||
include_commands = request.args.get('commands', 'false')
|
||||
if include_commands.lower() not in {'0', 'false'}:
|
||||
commands = [cmd for cmd in build_logs.get_commands(build.uuid)]
|
||||
response_obj['commands'] = commands
|
||||
if commands:
|
||||
last_command = commands[-1]
|
||||
elif start_param is None:
|
||||
last_command = build_logs.get_last_command(build.uuid)
|
||||
|
||||
if start_param is None:
|
||||
if last_command:
|
||||
start = last_command['index']
|
||||
else:
|
||||
start = 0
|
||||
else:
|
||||
start = int(start_param)
|
||||
|
||||
count, logs = build_logs.get_log_entries(build.uuid, start, end)
|
||||
|
||||
if start < 0:
|
||||
start = max(0, count + start)
|
||||
|
||||
if end < 0:
|
||||
end = count + end
|
||||
count, logs = build_logs.get_log_entries(build.uuid, start)
|
||||
|
||||
response_obj.update({
|
||||
'start': start,
|
||||
'end': end,
|
||||
'total': count,
|
||||
'logs': [log for log in logs],
|
||||
})
|
||||
|
@ -1239,18 +1222,28 @@ def request_repo_build(namespace, repository):
|
|||
logger.debug('User requested repository initialization.')
|
||||
dockerfile_id = request.get_json()['file_id']
|
||||
|
||||
# Check if the dockerfile resource has already been used. If so, then it can only be reused if the
|
||||
# user has access to the repository for which it was used.
|
||||
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
||||
if associated_repository:
|
||||
if not ModifyRepositoryPermission(associated_repository.namespace, associated_repository.name):
|
||||
abort(403)
|
||||
|
||||
# Start the build.
|
||||
repo = model.get_repository(namespace, repository)
|
||||
token = model.create_access_token(repo, 'write')
|
||||
display_name = user_files.get_file_checksum(dockerfile_id)
|
||||
logger.debug('**********Md5: %s' % display_name)
|
||||
|
||||
host = urlparse.urlparse(request.url).netloc
|
||||
tag = '%s/%s/%s' % (host, repo.namespace, repo.name)
|
||||
build_request = model.create_repository_build(repo, token, dockerfile_id,
|
||||
tag)
|
||||
tag, display_name)
|
||||
dockerfile_build_queue.put(json.dumps({
|
||||
'build_uuid': build_request.uuid,
|
||||
'namespace': namespace,
|
||||
'repository': repository,
|
||||
}))
|
||||
}), retries_remaining=1)
|
||||
|
||||
log_action('build_dockerfile', namespace,
|
||||
{'repo': repository, 'namespace': namespace,
|
||||
|
@ -1302,7 +1295,11 @@ def create_webhook(namespace, repository):
|
|||
def get_webhook(namespace, repository, public_id):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
webhook = model.get_webhook(namespace, repository, public_id)
|
||||
try:
|
||||
webhook = model.get_webhook(namespace, repository, public_id)
|
||||
except model.InvalidWebhookException:
|
||||
abort(404)
|
||||
|
||||
return jsonify(webhook_view(webhook))
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1697,7 +1694,11 @@ def list_repo_tokens(namespace, repository):
|
|||
def get_tokens(namespace, repository, code):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
perm = model.get_repo_delegate_token(namespace, repository, code)
|
||||
try:
|
||||
perm = model.get_repo_delegate_token(namespace, repository, code)
|
||||
except model.InvalidTokenException:
|
||||
abort(404)
|
||||
|
||||
return jsonify(token_view(perm))
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1834,6 +1835,8 @@ def set_card(user, token):
|
|||
cus.save()
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
except stripe.InvalidRequestError as e:
|
||||
return carderror_response(e)
|
||||
|
||||
return get_card(user)
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import urlparse
|
|||
from flask import request, make_response, jsonify, session, Blueprint
|
||||
from functools import wraps
|
||||
|
||||
from data import model
|
||||
from data import model, userevent
|
||||
from data.queue import webhook_queue
|
||||
from app import mixpanel
|
||||
from app import mixpanel, app
|
||||
from auth.auth import (process_auth, get_authenticated_user,
|
||||
get_validated_token)
|
||||
from util.names import parse_repository_name
|
||||
|
@ -80,8 +80,16 @@ def create_user():
|
|||
if existing_user:
|
||||
verified = model.verify_user(username, password)
|
||||
if verified:
|
||||
# Mark that the user was logged in.
|
||||
event = app.config['USER_EVENTS'].get_event(username)
|
||||
event.publish_event_data('docker-cli', {'action': 'login'})
|
||||
|
||||
return make_response('Verified', 201)
|
||||
else:
|
||||
# Mark that the login failed.
|
||||
event = app.config['USER_EVENTS'].get_event(username)
|
||||
event.publish_event_data('docker-cli', {'action': 'loginfailure'})
|
||||
|
||||
abort(400, 'Invalid password.', issue='login-failure')
|
||||
|
||||
else:
|
||||
|
@ -186,9 +194,21 @@ def create_repository(namespace, repository):
|
|||
}
|
||||
|
||||
if get_authenticated_user():
|
||||
mixpanel.track(get_authenticated_user().username, 'push_repo',
|
||||
extra_params)
|
||||
metadata['username'] = get_authenticated_user().username
|
||||
username = get_authenticated_user().username
|
||||
|
||||
mixpanel.track(username, 'push_repo', extra_params)
|
||||
metadata['username'] = username
|
||||
|
||||
# Mark that the user has started pushing the repo.
|
||||
user_data = {
|
||||
'action': 'push_repo',
|
||||
'repository': repository,
|
||||
'namespace': namespace
|
||||
}
|
||||
|
||||
event = app.config['USER_EVENTS'].get_event(username)
|
||||
event.publish_event_data('docker-cli', user_data)
|
||||
|
||||
else:
|
||||
mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
|
||||
metadata['token'] = get_validated_token().friendly_name
|
||||
|
@ -222,6 +242,21 @@ def update_images(namespace, repository):
|
|||
updated_tags[image['Tag']] = image['id']
|
||||
model.set_image_checksum(image['id'], repo, image['checksum'])
|
||||
|
||||
if get_authenticated_user():
|
||||
username = get_authenticated_user().username
|
||||
|
||||
# Mark that the user has pushed the repo.
|
||||
user_data = {
|
||||
'action': 'pushed_repo',
|
||||
'repository': repository,
|
||||
'namespace': namespace
|
||||
}
|
||||
|
||||
event = app.config['USER_EVENTS'].get_event(username)
|
||||
event.publish_event_data('docker-cli', user_data)
|
||||
|
||||
num_removed = model.garbage_collect_repository(namespace, repository)
|
||||
|
||||
# Generate a job for each webhook that has been added to this repo
|
||||
webhooks = model.list_webhooks(namespace, repository)
|
||||
for webhook in webhooks:
|
||||
|
@ -237,7 +272,8 @@ def update_images(namespace, repository):
|
|||
'homepage': 'https://quay.io/repository/%s' % repo_string,
|
||||
'visibility': repo.visibility.name,
|
||||
'updated_tags': updated_tags,
|
||||
'pushed_image_count': len(image_with_checksums),
|
||||
'pushed_image_count': len(image_with_checksums),
|
||||
'pruned_image_count': num_removed,
|
||||
}
|
||||
webhook_queue.put(json.dumps(webhook_data))
|
||||
|
||||
|
|
81
endpoints/realtime.py
Normal file
81
endpoints/realtime.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import logging
|
||||
import redis
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
from flask import request, make_response, Blueprint, abort, Response
|
||||
from flask.ext.login import current_user, logout_user
|
||||
from data import model, userevent
|
||||
from app import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
realtime = Blueprint('realtime', __name__)
|
||||
|
||||
def api_login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if not current_user.is_authenticated():
|
||||
abort(401)
|
||||
|
||||
if (current_user and current_user.db_user() and
|
||||
current_user.db_user().organization):
|
||||
abort(401)
|
||||
|
||||
if (current_user and current_user.db_user() and
|
||||
current_user.db_user().robot):
|
||||
abort(401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_view
|
||||
|
||||
|
||||
@realtime.route("/user/")
|
||||
@api_login_required
|
||||
def index():
|
||||
debug_template = """
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Server sent events</h1>
|
||||
<div id="event"></div>
|
||||
<script type="text/javascript">
|
||||
|
||||
var eventOutputContainer = document.getElementById("event");
|
||||
var evtSrc = new EventSource("/realtime/user/subscribe?events=docker-cli");
|
||||
|
||||
evtSrc.onmessage = function(e) {
|
||||
console.log(e.data);
|
||||
eventOutputContainer.innerHTML = e.data;
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return(debug_template)
|
||||
|
||||
|
||||
@realtime.route("/user/test")
|
||||
@api_login_required
|
||||
def user_test():
|
||||
evt = userevent.UserEvent('logs.quay.io', current_user.db_user().username)
|
||||
evt.publish_event_data('test', {'foo': 2})
|
||||
return 'OK'
|
||||
|
||||
@realtime.route("/user/subscribe")
|
||||
@api_login_required
|
||||
def user_subscribe():
|
||||
def wrapper(listener):
|
||||
for event_id, data in listener.event_stream():
|
||||
message = {'event': event_id, 'data': data}
|
||||
json_string = json.dumps(message)
|
||||
yield 'data: %s\n\n' % json_string
|
||||
|
||||
events = request.args.get('events', '').split(',')
|
||||
if not events:
|
||||
abort(404)
|
||||
|
||||
listener = userevent.UserEventListener('logs.quay.io', current_user.db_user().username, events)
|
||||
return Response(wrapper(listener), mimetype="text/event-stream")
|
|
@ -73,22 +73,8 @@ def delete_tag(namespace, repository, tag):
|
|||
|
||||
if permission.can():
|
||||
model.delete_tag(namespace, repository, tag)
|
||||
model.garbage_collect_repository(namespace, repository)
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@tags.route('/repositories/<path:repository>/tags',
|
||||
methods=['DELETE'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
def delete_repository_tags(namespace, repository):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
|
||||
if permission.can():
|
||||
model.delete_all_repository_tags(namespace, repository)
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
return make_response('Deleted', 200)
|
||||
|
||||
abort(403)
|
||||
|
|
|
@ -63,6 +63,12 @@ def guide():
|
|||
return index('')
|
||||
|
||||
|
||||
@web.route('/tutorial/')
|
||||
@no_cache
|
||||
def tutorial():
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/organizations/')
|
||||
@web.route('/organizations/new/')
|
||||
@no_cache
|
||||
|
@ -88,6 +94,12 @@ def contact():
|
|||
return index('')
|
||||
|
||||
|
||||
@web.route('/about/')
|
||||
@no_cache
|
||||
def about():
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/new/')
|
||||
@no_cache
|
||||
def new():
|
||||
|
|
|
@ -277,7 +277,8 @@ def populate_database():
|
|||
|
||||
token = model.create_access_token(building, 'write')
|
||||
tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
|
||||
build = model.create_repository_build(building, token, '123-45-6789', tag)
|
||||
build = model.create_repository_build(building, token, '123-45-6789', tag,
|
||||
'build-name')
|
||||
build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef'
|
||||
build.save()
|
||||
|
||||
|
|
1
run-local.sh
Executable file
1
run-local.sh
Executable file
|
@ -0,0 +1 @@
|
|||
gunicorn -c conf/gunicorn_local.py application:application
|
|
@ -543,6 +543,10 @@ i.toggle-icon:hover {
|
|||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.active .build-tab-link .phase-icon {
|
||||
box-shadow: 0px 0px 10px #FFFFFF, 0px 0px 10px #FFFFFF;
|
||||
}
|
||||
|
||||
.build-status .phase-icon {
|
||||
margin-top: 4px;
|
||||
float: left;
|
||||
|
@ -842,9 +846,19 @@ i.toggle-icon:hover {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.visible-md-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-sm-inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 991px) {
|
||||
.visible-md-inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) and (min-width: 768px) {
|
||||
.visible-sm-inline {
|
||||
|
@ -1445,7 +1459,10 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo .header {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -1495,36 +1512,12 @@ p.editable:hover i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo .status-boxes {
|
||||
float: right;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box {
|
||||
cursor: pointer;
|
||||
.repo .repo-controls .count {
|
||||
display: inline-block;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .title {
|
||||
padding: 4px;
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .title i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .count {
|
||||
display: inline-block;
|
||||
background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);
|
||||
padding: 4px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
font-weight: bold;
|
||||
color: #428bca;
|
||||
|
||||
transform: scaleX(0);
|
||||
-webkit-transform: scaleX(0);
|
||||
|
@ -1535,13 +1528,13 @@ p.editable:hover i {
|
|||
-moz-transition: -moz-transform 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo .status-boxes .status-box .count.visible {
|
||||
.repo .repo-controls .count.visible {
|
||||
transform: scaleX(1);
|
||||
-webkit-transform: scaleX(1);
|
||||
-moz-transform: scaleX(1);
|
||||
}
|
||||
|
||||
.repo .pull-command {
|
||||
.repo .repo-controls {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
|
@ -1745,69 +1738,114 @@ p.editable:hover i {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.repo-build .build-pane .build-logs .command-logs {
|
||||
margin: 10px;
|
||||
padding-right: 10px;
|
||||
.repo-build .build-pane .build-logs .container-header {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-entry,
|
||||
.repo-build .build-pane .build-logs .log-entry {
|
||||
.repo-build .build-pane .build-logs .container-logs {
|
||||
margin: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-title,
|
||||
.repo-build .build-pane .build-logs .log-entry .message,
|
||||
.repo-build .build-pane .build-logs .log-entry .message span {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-entry {
|
||||
.repo-build .build-pane .build-logs .container-header {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-entry i.fa.chevron {
|
||||
.repo-build .build-pane .build-logs .container-header i.fa.chevron {
|
||||
color: #666;
|
||||
margin-right: 4px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
top: 6px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-entry .label {
|
||||
text-align: center;
|
||||
.repo-build .build-pane .build-logs .log-container.command {
|
||||
margin-left: 42px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header.building {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header.pushing {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 0, 0, 0.17);
|
||||
border-radius: 10px;
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element i.fa {
|
||||
color: red;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.repo-build .build-log-error-element .error-message {
|
||||
display: inline-block;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header .label {
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
width: 86px;
|
||||
display: inline-block;
|
||||
background-color: #aaa;
|
||||
|
||||
border-right: 4px solid #aaa;
|
||||
background-color: #444;
|
||||
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
top: 4px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .command-entry .command-title {
|
||||
.repo-build .build-pane .build-logs .container-header .container-content {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .container-header .container-content.build-log-command {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
.label.FROM {
|
||||
background-color: #5bc0de !important;
|
||||
border-color: #5bc0de !important;
|
||||
}
|
||||
|
||||
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
|
||||
background-color: #428bca !important;
|
||||
border-color: #428bca !important;
|
||||
}
|
||||
|
||||
.label.RUN, .label.ADD {
|
||||
background-color: #5cb85c !important;
|
||||
border-color: #5cb85c !important;
|
||||
}
|
||||
|
||||
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR {
|
||||
background-color: #f0ad4e !important;
|
||||
border-color: #f0ad4e !important;
|
||||
}
|
||||
|
||||
.label.MAINTAINER {
|
||||
background-color: #aaa !important;
|
||||
border-color: #aaa !important;
|
||||
}
|
||||
|
||||
.repo-build .build-pane .build-logs .log-entry {
|
||||
|
@ -2675,13 +2713,17 @@ p.editable:hover i {
|
|||
color: #888 !important;
|
||||
}
|
||||
|
||||
.repo-breadcrumb-element .current a {
|
||||
color: #444 !important;
|
||||
}
|
||||
|
||||
.repo-breadcrumb-element .crumb:after {
|
||||
content: "/";
|
||||
color: #ccc;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.repo-breadcrumb-element .crumb:hover, .repo-breadcrumb-element .crumb:hover a {
|
||||
.repo-breadcrumb-element .crumb:hover, .repo-breadcrumb-element .crumb:hover a, .repo-breadcrumb-element .current:hover a {
|
||||
color: #2a6496 !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -2912,4 +2954,241 @@ p.editable:hover i {
|
|||
|
||||
.contact-options {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/*********************************************/
|
||||
|
||||
.angular-tour-ui-element.overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 20px;
|
||||
border-radius: 10px;
|
||||
z-index: 9999999;
|
||||
|
||||
background: white;
|
||||
-webkit-box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 750ms ease-in-out;
|
||||
-webkit-transition: opacity 750ms ease-in-out;
|
||||
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay.touring {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay.nottouring {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .tour-title {
|
||||
background-color: #3276b1;
|
||||
color: white;
|
||||
padding: 4px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
border-radius: 4px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .tour-title h4 {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin: 0px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .step-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .step-content {
|
||||
padding: 10px;
|
||||
padding-left: 0px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .tour-contents {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .controls {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.overlay .controls .btn {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.angular-tour-ui-element.overlay .fa {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************/
|
||||
|
||||
.angular-tour-ui-element.inline {
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.inline .fa-dot-circle-o {
|
||||
font-size: 34px;
|
||||
background: #ddd;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
padding-top: 4px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.inline .tour-title h4 {
|
||||
font-size: 28px;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.inline .tour-title .close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.angular-tour-ui-element.inline .step-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.inline .step-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element.inline .controls {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element .wait-message {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element .wait-message .quay-spinner {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element .wait-message .quay-spinner .small-spinner {
|
||||
border-top-color: #999;
|
||||
border-left-color: #999;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element .note {
|
||||
margin: 10px;
|
||||
padding: 6px;
|
||||
background: #eee;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.angular-tour-ui-element .skip-message {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
pre.command {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
text-shadow: none;
|
||||
overflow: auto;
|
||||
border: solid 1px #ccc;
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
pre.command:before {
|
||||
content: "\f120";
|
||||
font-family: "FontAwesome";
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/*********************************************/
|
||||
|
||||
.contact-options .option-twitter .fa-circle {
|
||||
color: #00b0ed;
|
||||
}
|
||||
|
||||
.contact-options .option-phone .fa-circle {
|
||||
color: #1dd924;
|
||||
}
|
||||
|
||||
.contact-options .option-irc .fa-circle {
|
||||
color: #e52f00;
|
||||
}
|
||||
|
||||
.contact-options .option-email .fa-circle {
|
||||
color: #1b72f1;
|
||||
}
|
||||
|
||||
.about-us .row {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.about-basic-icon {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.about-basic-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo-name-small:before {
|
||||
content: "\f0a0";
|
||||
font-family: FontAwesome;
|
||||
font-size: 20px;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.repo-name-small {
|
||||
background: none repeat scroll 0 0 #D9EDF7;
|
||||
border-radius: 10px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.file-drop {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
28
static/directives/angular-tour-ui.html
Normal file
28
static/directives/angular-tour-ui.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<div class="angular-tour-ui-element"
|
||||
ng-class="[tour ? 'touring' : 'nottouring', inline ? 'inline' : 'overlay']">
|
||||
|
||||
<div class="tour-title">
|
||||
<h4><i class="fa fa-dot-circle-o"></i> {{ tour.title }}</h4>
|
||||
<button type="button" class="close" ng-click="stop()" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tour-contents">
|
||||
<div class="step-title" ng-shpw="step.title">{{ step.title }}</div>
|
||||
<div class="slide-animate-container">
|
||||
<div class="step-content" ng-show="step.content" ng-bind-html="step.content || ''"></div>
|
||||
<div class="step-content">
|
||||
<ng:include src="step.templateUrl" scope="info"></ng:include>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn btn-primary" ng-click="next()" ng-show="hasNextStep && !step.signal">Next</button>
|
||||
<button class="btn btn-primary" ng-click="stop()" ng-show="!hasNextStep && !inline">Done</button>
|
||||
</div>
|
||||
|
||||
<div class="wait-message" ng-show="step.waitMessage">
|
||||
<div class="quay-spinner"></div> {{ step.waitMessage }}
|
||||
<span class="skip-message" ng-show="step.skipTitle"><button class="btn btn-default" ng-click="next()">{{ step.skipTitle }}</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
6
static/directives/build-log-command.html
Normal file
6
static/directives/build-log-command.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<span class="command" bindonce>
|
||||
<span class="label" bo-class="getCommandKind(command.message)" bo-show="getCommandKind(command.message)"
|
||||
bo-text="getCommandKind(command.message)">
|
||||
</span>
|
||||
<span class="command-title" bo-html="getCommandTitleHtml(command.message)"></span>
|
||||
</span>
|
4
static/directives/build-log-error.html
Normal file
4
static/directives/build-log-error.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span bindonce class="build-log-error-element">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span class="error-message" bo-text="error.message"></span>
|
||||
</span>
|
4
static/directives/build-log-phase.html
Normal file
4
static/directives/build-log-phase.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span bindonce class="build-log-phase-element">
|
||||
<span class="phase-icon" ng-class="phase.message"></span>
|
||||
<span class="build-message" phase="phase.message"></span>
|
||||
</span>
|
|
@ -1 +1 @@
|
|||
<span class="build-message-element">{{ getBuildMessage(build) }}</span>
|
||||
<span class="build-message-element">{{ getBuildMessage(phase) }}</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="build-progress-element">
|
||||
<div class="progress" ng-class="getPercentage(build) < 100 ? 'active progress-striped' : ''" ng-show="getPercentage(build) > 0 && getPercentage(build) < 100">
|
||||
<div class="progress" ng-class="getPercentage(build) < 100 ? 'active progress-striped' : ''">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ getPercentage(build) }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + getPercentage(build) + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div id="build-status-container" class="build-status-container">
|
||||
<div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span class="build-message" build="build"></span>
|
||||
<span class="build-message" phase="build.phase"></span>
|
||||
</div>
|
||||
<div class="timing">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
|
|
27
static/directives/dockerfile-build-dialog.html
Normal file
27
static/directives/dockerfile-build-dialog.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<div class="dockerfile-build-dialog-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="dockerfilebuildModal">
|
||||
<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">
|
||||
Start new Dockerfile build
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body token-dialog-body">
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="dockerfile-build-form" repository="repository" upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted(build)" build-failed="handleBuildFailed(message)" start-now="startCounter"
|
||||
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="startBuild()" ng-disabled="building || uploading || !hasDockerfile">Start Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
19
static/directives/dockerfile-build-form.html
Normal file
19
static/directives/dockerfile-build-form.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class="dockerfile-build-form-element">
|
||||
<div class="container" ng-show="building">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
<div class="container" ng-show="uploading">
|
||||
<span class="message">Uploading file {{ upload_file }}</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" ng-show="!uploading && !building">
|
||||
<div class="init-description">
|
||||
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||
</div>
|
||||
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
|
||||
</div>
|
||||
</div>
|
|
@ -15,7 +15,8 @@
|
|||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
|
||||
<li><a href="http://docs.quay.io/">Documentation</a></li>
|
||||
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
|
||||
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
|
||||
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
|
||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<span class="repo-breadcrumb-element">
|
||||
<span ng-show="!image">
|
||||
<span ng-show="!image && !subsection">
|
||||
<span class="crumb">
|
||||
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
|
||||
</span>
|
||||
<span class="current">{{repo.name}}</span>
|
||||
</span>
|
||||
<span ng-show="image">
|
||||
<span ng-show="image && !subsection">
|
||||
<span class="crumb">
|
||||
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
|
||||
</span>
|
||||
<span class="crumb"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}">{{repo.name}}</a></span>
|
||||
<span class="current">{{image.id.substr(0, 12)}}</span>
|
||||
<span class="current">
|
||||
<i class="fa fa-archive" style="margin-left: 2px; margin-right: 2px;"></i>
|
||||
{{image.id.substr(0, 12)}}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-show="!image && subsection">
|
||||
<span class="crumb">
|
||||
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
|
||||
</span>
|
||||
<span class="crumb"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}">{{repo.name}}</a></span>
|
||||
<span class="current"><i class="fa" ng-class="subsectionIcon" style="margin-left: 4px; margin-right: 8px;"></i>{{subsection}}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
|
400
static/js/app.js
400
static/js/app.js
|
@ -102,8 +102,7 @@ function getMarkedDown(string) {
|
|||
return Markdown.getSanitizingConverter().makeHtml(string || '');
|
||||
}
|
||||
|
||||
// Start the application code itself.
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce'], function($provide, cfpLoadingBarProvider) {
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml'], function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
|
@ -439,6 +438,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
var planService = {};
|
||||
var listeners = [];
|
||||
|
||||
var previousSubscribeFailure = false;
|
||||
|
||||
planService.getFreePlan = function() {
|
||||
return 'free';
|
||||
};
|
||||
|
@ -628,12 +629,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
||||
|
||||
planService.getCardInfo(orgname, function(cardInfo) {
|
||||
if (plan.price > 0 && !cardInfo.last4) {
|
||||
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
|
||||
return;
|
||||
}
|
||||
|
||||
previousSubscribeFailure = false;
|
||||
|
||||
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
||||
previousSubscribeFailure = true;
|
||||
planService.handleCardError(resp);
|
||||
callbacks['failure'](resp);
|
||||
});
|
||||
|
@ -793,13 +797,16 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
reloadOnSearch: false, controller: UserAdminCtrl}).
|
||||
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html',
|
||||
controller: GuideCtrl}).
|
||||
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html',
|
||||
controller: TutorialCtrl}).
|
||||
when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html',
|
||||
controller: ContactCtrl}).
|
||||
when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}).
|
||||
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
|
||||
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
|
||||
templateUrl: '/static/partials/security.html', controller: SecurityCtrl}).
|
||||
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
||||
templateUrl: '/static/partials/security.html'}).
|
||||
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html'}).
|
||||
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
|
||||
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
||||
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
||||
|
@ -885,7 +892,9 @@ quayApp.directive('repoBreadcrumb', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'repo': '=repo',
|
||||
'image': '=image'
|
||||
'image': '=image',
|
||||
'subsection': '=subsection',
|
||||
'subsectionIcon': '=subsectionIcon'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
|
@ -1134,6 +1143,13 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.filter('reverse', function() {
|
||||
return function(items) {
|
||||
return items.slice().reverse();
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
|
||||
|
@ -2476,6 +2492,119 @@ quayApp.directive('namespaceSelector', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogPhase', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-phase.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogError', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-error.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'error': '=error'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('buildLogCommand', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-command.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'command': '=command'
|
||||
},
|
||||
controller: function($scope, $element, $sanitize) {
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||
},
|
||||
|
||||
'': function(pieces) {
|
||||
var rnamespace = pieces.length == 1 ? '_' : pieces[0];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return 'https://index.docker.io/u/' + rnamespace + '/' + rname + '/';
|
||||
}
|
||||
};
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': function(title) {
|
||||
var pieces = title.split('/');
|
||||
var registry = pieces.length < 3 ? '' : pieces[0];
|
||||
if (!registryHandlers[registry]) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getCommandKind = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
var title = getTitleWithoutStep(fullTitle);
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var space = title.indexOf(' ');
|
||||
return title.substring(0, space);
|
||||
};
|
||||
|
||||
$scope.getCommandTitleHtml = function(fullTitle) {
|
||||
var title = getTitleWithoutStep(fullTitle) || fullTitle;
|
||||
var space = title.indexOf(' ');
|
||||
if (space <= 0) {
|
||||
return $sanitize(title);
|
||||
}
|
||||
|
||||
var kind = $scope.getCommandKind(fullTitle);
|
||||
var sanitized = $sanitize(title.substring(space + 1));
|
||||
|
||||
var handler = kindHandlers[kind || ''];
|
||||
if (handler) {
|
||||
return handler(sanitized);
|
||||
} else {
|
||||
return sanitized;
|
||||
}
|
||||
};
|
||||
|
||||
var getTitleWithoutStep = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
if (colon <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $.trim(fullTitle.substring(colon + 1));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
quayApp.directive('buildStatus', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -2501,17 +2630,17 @@ quayApp.directive('buildMessage', function () {
|
|||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getBuildMessage = function (buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
$scope.getBuildMessage = function (phase) {
|
||||
switch (phase) {
|
||||
case 'starting':
|
||||
case 'initializing':
|
||||
return 'Starting Dockerfile build';
|
||||
|
||||
case 'waiting':
|
||||
return 'Waiting for available build worker.';
|
||||
return 'Waiting for available build worker';
|
||||
|
||||
case 'building':
|
||||
return 'Building image from Dockerfile';
|
||||
|
@ -2523,7 +2652,7 @@ quayApp.directive('buildMessage', function () {
|
|||
return 'Dockerfile build completed and pushed';
|
||||
|
||||
case 'error':
|
||||
return 'Dockerfile build failed.';
|
||||
return 'Dockerfile build failed';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2571,6 +2700,239 @@ quayApp.directive('buildProgress', function () {
|
|||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerfileBuildDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'showNow': '=showNow',
|
||||
'buildStarted': '&buildStarted'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
$scope.startCounter = 0;
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
$('#dockerfilebuildModal').modal('hide');
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': build});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleBuildFailed = function(message) {
|
||||
$scope.errorMessage = message;
|
||||
};
|
||||
|
||||
$scope.startBuild = function() {
|
||||
$scope.errorMessage = null;
|
||||
$scope.startCounter++;
|
||||
};
|
||||
|
||||
$scope.$watch('showNow', function(sn) {
|
||||
if (sn && $scope.repository) {
|
||||
$('#dockerfilebuildModal').modal({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerfileBuildForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-form.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'startNow': '=startNow',
|
||||
'hasDockerfile': '=hasDockerfile',
|
||||
'uploadFailed': '&uploadFailed',
|
||||
'uploadStarted': '&uploadStarted',
|
||||
'buildStarted': '&buildStarted',
|
||||
'buildFailed': '&buildFailed',
|
||||
'missingFile': '&missingFile',
|
||||
'uploading': '=uploading',
|
||||
'building': '=building'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.internal = {'hasDockerfile': false};
|
||||
|
||||
var handleBuildFailed = function(message) {
|
||||
message = message || 'Dockerfile build failed to start';
|
||||
|
||||
var result = false;
|
||||
if ($scope.buildFailed) {
|
||||
result = $scope.buildFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot start Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleUploadFailed = function(message) {
|
||||
message = message || 'Error with file upload';
|
||||
|
||||
var result = false;
|
||||
if ($scope.uploadFailed) {
|
||||
result = $scope.uploadFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot upload file for Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleMissingFile = function() {
|
||||
var result = false;
|
||||
if ($scope.missingFile) {
|
||||
result = $scope.missingFile({});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": 'A Dockerfile or an archive containing a Dockerfile is required',
|
||||
"title": "Missing Dockerfile",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var startBuild = function(fileId) {
|
||||
$scope.building = true;
|
||||
|
||||
var repo = $scope.repository;
|
||||
var data = {
|
||||
'file_id': fileId
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': resp});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
handleBuildFailed(resp.message);
|
||||
});
|
||||
};
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType) {
|
||||
if ($scope.uploadStarted) {
|
||||
$scope.uploadStarted({});
|
||||
}
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
handleUploadFailed();
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
startBuild(fileId);
|
||||
$scope.uploading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var startFileUpload = function(repo) {
|
||||
$scope.uploading = true;
|
||||
$scope.uploading_progress = 0;
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if (uploader.files.length == 0) {
|
||||
handleMissingFile();
|
||||
$scope.uploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var file = uploader.files[0];
|
||||
$scope.upload_file = file.name;
|
||||
|
||||
var mimeType = file.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
handleUploadFailed('Could not retrieve upload URL');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('internal.hasDockerfile', function(d) {
|
||||
$scope.hasDockerfile = d;
|
||||
});
|
||||
|
||||
$scope.$watch('startNow', function() {
|
||||
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
|
||||
startFileUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
|
||||
quayApp.directive('ngBlur', function() {
|
||||
return function( scope, elem, attrs ) {
|
||||
|
@ -2580,6 +2942,22 @@ quayApp.directive('ngBlur', function() {
|
|||
};
|
||||
});
|
||||
|
||||
quayApp.directive("filePresent", [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
'filePresent': "="
|
||||
},
|
||||
link: function (scope, element, attributes) {
|
||||
element.bind("change", function (changeEvent) {
|
||||
scope.$apply(function() {
|
||||
scope.filePresent = changeEvent.target.files.length > 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
quayApp.directive('ngVisible', function () {
|
||||
return function (scope, element, attr) {
|
||||
scope.$watch(attr.ngVisible, function (visible) {
|
||||
|
|
|
@ -15,8 +15,14 @@ $.fn.clipboardCopy = function() {
|
|||
});
|
||||
};
|
||||
|
||||
function SigninCtrl($scope) {
|
||||
};
|
||||
function GuideCtrl() {
|
||||
}
|
||||
|
||||
function SecurityCtrl($scope) {
|
||||
}
|
||||
|
||||
function ContactCtrl($scope) {
|
||||
}
|
||||
|
||||
function PlansCtrl($scope, $location, UserService, PlanService) {
|
||||
// Load the list of plans.
|
||||
|
@ -42,13 +48,153 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
|
|||
};
|
||||
}
|
||||
|
||||
function GuideCtrl($scope) {
|
||||
}
|
||||
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
|
||||
// Default to showing sudo on all commands if on linux.
|
||||
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
|
||||
|
||||
function SecurityCtrl($scope) {
|
||||
}
|
||||
|
||||
function ContactCtrl($scope) {
|
||||
$scope.tour = {
|
||||
'title': 'Quay.io Tutorial',
|
||||
'initialScope': {
|
||||
'showSudo': showSudo
|
||||
},
|
||||
'steps': [
|
||||
{
|
||||
'title': 'Welcome to the Quay.io tutorial!',
|
||||
'templateUrl': '/static/tutorial/welcome.html'
|
||||
},
|
||||
{
|
||||
'title': 'Sign in to get started',
|
||||
'templateUrl': '/static/tutorial/signup.html',
|
||||
'signal': function($tourScope) {
|
||||
var user = UserService.currentUser();
|
||||
$tourScope.username = user.username;
|
||||
$tourScope.email = user.email;
|
||||
$tourScope.inOrganization = user.organizations && user.organizations.length > 0;
|
||||
return !user.anonymous;
|
||||
}
|
||||
},
|
||||
{
|
||||
'title': 'Step 1: Login to Quay.io',
|
||||
'templateUrl': '/static/tutorial/docker-login.html',
|
||||
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
|
||||
function(message) {
|
||||
return message['data']['action'] == 'login';
|
||||
}),
|
||||
'waitMessage': "Waiting for docker login",
|
||||
'skipTitle': "I'm already logged in",
|
||||
'mixpanelEvent': 'tutorial_start'
|
||||
},
|
||||
{
|
||||
'title': 'Step 2: Create a new container',
|
||||
'templateUrl': '/static/tutorial/create-container.html'
|
||||
},
|
||||
{
|
||||
'title': 'Step 3: Create a new image',
|
||||
'templateUrl': '/static/tutorial/create-image.html'
|
||||
},
|
||||
{
|
||||
'title': 'Step 4: Push the image to Quay.io',
|
||||
'templateUrl': '/static/tutorial/push-image.html',
|
||||
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
|
||||
function(message, tourScope) {
|
||||
var pushing = message['data']['action'] == 'push_repo';
|
||||
if (pushing) {
|
||||
tourScope.repoName = message['data']['repository'];
|
||||
}
|
||||
return pushing;
|
||||
}),
|
||||
'waitMessage': "Waiting for repository push to begin",
|
||||
'mixpanelEvent': 'tutorial_wait_for_push'
|
||||
},
|
||||
{
|
||||
'title': 'Push in progress',
|
||||
'templateUrl': '/static/tutorial/pushing.html',
|
||||
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
|
||||
function(message, tourScope) {
|
||||
return message['data']['action'] == 'pushed_repo';
|
||||
}),
|
||||
'waitMessage': "Waiting for repository push to complete"
|
||||
},
|
||||
{
|
||||
'title': 'Step 5: View the repository on Quay.io',
|
||||
'templateUrl': '/static/tutorial/view-repo.html',
|
||||
'signal': AngularTourSignals.matchesLocation('/repository/'),
|
||||
'overlayable': true,
|
||||
'mixpanelEvent': 'tutorial_push_complete'
|
||||
},
|
||||
{
|
||||
'templateUrl': '/static/tutorial/view-repo.html',
|
||||
'signal': AngularTourSignals.matchesLocation('/repository/'),
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'templateUrl': '/static/tutorial/waiting-repo-list.html',
|
||||
'signal': AngularTourSignals.elementAvaliable('*[data-repo="{{username}}/{{repoName}}"]'),
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'templateUrl': '/static/tutorial/repo-list.html',
|
||||
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}'),
|
||||
'element': '*[data-repo="{{username}}/{{repoName}}"]',
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'title': 'Repository View',
|
||||
'content': 'This is the repository view page. It displays all the primary information about your repository.',
|
||||
'overlayable': true,
|
||||
'mixpanelEvent': 'tutorial_view_repo'
|
||||
},
|
||||
{
|
||||
'title': 'Image History',
|
||||
'content': 'The tree displays the full history of your repository, including all its tag. ' +
|
||||
'You can click on a tag or image to see its information.',
|
||||
'element': '#image-history-container',
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'title': 'Tag/Image Information',
|
||||
'content': 'This panel displays information about the currently selected tag or image',
|
||||
'element': '#side-panel',
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'title': 'Select tag or image',
|
||||
'content': 'You can select a tag or image by clicking on this dropdown',
|
||||
'element': '#side-panel-dropdown',
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'content': 'To view the admin settings for the repository, click on the gear',
|
||||
'element': '#admin-cog',
|
||||
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}/admin'),
|
||||
'overlayable': true
|
||||
},
|
||||
{
|
||||
'title': 'Repository Admin',
|
||||
'content': "The repository admin panel allows for modification of a repository's permissions, webhooks, visibility and other settings",
|
||||
'overlayable': true,
|
||||
'mixpanelEvent': 'tutorial_view_admin'
|
||||
},
|
||||
{
|
||||
'title': 'Permissions',
|
||||
'templateUrl': '/static/tutorial/permissions.html',
|
||||
'overlayable': true,
|
||||
'element': '#permissions'
|
||||
},
|
||||
{
|
||||
'title': 'Adding a permission',
|
||||
'content': 'To add an <b>additional</b> permission, enter a username or robot account name into the autocomplete ' +
|
||||
'or hit the dropdown arrow to manage robot accounts',
|
||||
'overlayable': true,
|
||||
'element': '#add-entity-permission'
|
||||
},
|
||||
{
|
||||
'templateUrl': '/static/tutorial/done.html',
|
||||
'overlayable': true,
|
||||
'mixpanelEvent': 'tutorial_complete'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
|
||||
|
@ -195,8 +341,17 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
// Start scope methods //////////////////////////////////////////
|
||||
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
startBuildInfoTimer($scope.repo);
|
||||
};
|
||||
|
||||
$scope.showBuild = function(buildInfo) {
|
||||
$location.path('/repository/' + namespace + '/' + name + '/build');
|
||||
$location.search('current', buildInfo.id);
|
||||
|
@ -619,37 +774,11 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
loadViewInfo();
|
||||
}
|
||||
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize) {
|
||||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var pollTimerHandle = null;
|
||||
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||
},
|
||||
|
||||
'': function(pieces) {
|
||||
var rnamespace = pieces.length == 1 ? '_' : pieces[0];
|
||||
var rname = pieces[pieces.length - 1];
|
||||
return 'https://index.docker.io/' + rnamespace + '/' + rname + '/';
|
||||
}
|
||||
};
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': function(title) {
|
||||
var pieces = title.split('/');
|
||||
var registry = pieces.length < 2 ? '' : pieces[0];
|
||||
if (!registryHandlers[registry]) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '">' + title + '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopPollTimer();
|
||||
});
|
||||
|
@ -664,73 +793,87 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.builds = [];
|
||||
$scope.polling = false;
|
||||
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(newBuild) {
|
||||
$scope.builds.push(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
$('.build-logs').height($(window).height() - 365);
|
||||
$('.build-logs').height($(window).height() - 385);
|
||||
};
|
||||
|
||||
$scope.getShortId = function(id) {
|
||||
var lastIndex = id.lastIndexOf('-');
|
||||
return id.substr(lastIndex + 1);
|
||||
$scope.askRestartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal({});
|
||||
};
|
||||
|
||||
$scope.getCommandKind = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
var title = getTitleWithoutStep(fullTitle);
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
$scope.restartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal('hide');
|
||||
|
||||
var space = title.indexOf(' ');
|
||||
return title.substring(0, space);
|
||||
var data = {
|
||||
'file_id': build['resource_key']
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
||||
$scope.builds.push(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getCommandTitleHtml = function(fullTitle) {
|
||||
var title = getTitleWithoutStep(fullTitle) || fullTitle;
|
||||
var space = title.indexOf(' ');
|
||||
if (space <= 0) {
|
||||
return $sanitize(title);
|
||||
}
|
||||
$scope.hasLogs = function(container) {
|
||||
return ((container.logs && container.logs.length) || (container._logs && container._logs.length));
|
||||
};
|
||||
|
||||
var kind = $scope.getCommandKind(fullTitle);
|
||||
var sanitized = $sanitize(title.substring(space + 1));
|
||||
|
||||
var handler = kindHandlers[kind || ''];
|
||||
if (handler) {
|
||||
return handler(sanitized);
|
||||
$scope.toggleLogs = function(container) {
|
||||
if (container._logs) {
|
||||
container.logs = container._logs;
|
||||
container._logs = null;
|
||||
} else {
|
||||
return sanitized;
|
||||
container._logs = container.logs;
|
||||
container.logs = null;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleCommand = function(command) {
|
||||
command.expanded = !command.expanded;
|
||||
|
||||
if (command.expanded && !command.logs) {
|
||||
// Load the logs for the command.
|
||||
loadCommandLogs(command);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
||||
// Find the build.
|
||||
for (var i = 0; i < $scope.builds.length; ++i) {
|
||||
if ($scope.builds[i].id == buildId) {
|
||||
$scope.setCurrentBuildInternal($scope.builds[i], opt_updateURL);
|
||||
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(build, opt_updateURL) {
|
||||
$scope.processANSI = function(message, container) {
|
||||
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
||||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message);
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
|
||||
if (build == $scope.currentBuild) { return; }
|
||||
|
||||
stopPollTimer();
|
||||
|
||||
$scope.commands = null;
|
||||
$scope.commandMap = {};
|
||||
$scope.logs = null;
|
||||
$scope.logStartIndex = 0;
|
||||
$scope.logEntries = null;
|
||||
$scope.logStartIndex = null;
|
||||
$scope.currentParentEntry = null;
|
||||
|
||||
$scope.currentBuild = build;
|
||||
$scope.currentBuildIndex = index;
|
||||
|
||||
if (opt_updateURL) {
|
||||
if (build) {
|
||||
|
@ -746,19 +889,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.adjustLogHeight();
|
||||
}, 1);
|
||||
|
||||
// Load the first set of logs.
|
||||
getBuildStatusAndLogs();
|
||||
|
||||
// If the build is currently processing, start the build timer.
|
||||
checkPollTimer();
|
||||
};
|
||||
|
||||
var getTitleWithoutStep = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
if (colon <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $.trim(fullTitle.substring(colon + 1));
|
||||
}
|
||||
|
||||
var checkPollTimer = function() {
|
||||
var build = $scope.currentBuild;
|
||||
if (!build) {
|
||||
|
@ -785,68 +922,29 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
var processLogs = function(logs, startIndex) {
|
||||
var currentCommand = $scope.commands.length > 0 ? $scope.commands[$scope.commands.length - 1] : null;
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var entry = logs[i];
|
||||
if (entry['is_command']) {
|
||||
var existing = $scope.commandMap[entry['message']];
|
||||
if (existing) {
|
||||
currentCommand = existing;
|
||||
continue;
|
||||
var type = entry['type'] || 'entry';
|
||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||
entry['_logs'] = [];
|
||||
entry['index'] = startIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
if ($scope.currentParentEntry['logs']) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
} else {
|
||||
$scope.currentParentEntry['_logs'].push(entry);
|
||||
}
|
||||
|
||||
currentCommand = {
|
||||
'message': entry['message'],
|
||||
'index': startIndex + i
|
||||
};
|
||||
$scope.commands.push(currentCommand);
|
||||
$scope.commandMap[entry['message']] = currentCommand;
|
||||
continue;
|
||||
}
|
||||
if (!currentCommand.logs) {
|
||||
currentCommand.logs = [];
|
||||
}
|
||||
currentCommand.logs.push(entry);
|
||||
}
|
||||
|
||||
if (currentCommand.expanded == null) {
|
||||
currentCommand.expanded = true;
|
||||
}
|
||||
};
|
||||
|
||||
var loadCommandLogs = function(command) {
|
||||
var start = command['index'] + 1;
|
||||
var end = null;
|
||||
|
||||
var currentCommandIndex = jQuery.inArray(command, $scope.commands);
|
||||
if (currentCommandIndex >= 0 && currentCommandIndex < $scope.commands.length - 1) {
|
||||
var nextCommand = $scope.commands[currentCommandIndex + 1];
|
||||
end = nextCommand.index ? nextCommand.index - 1 : null;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': $scope.currentBuild.id
|
||||
};
|
||||
|
||||
var options = {
|
||||
'start': start
|
||||
};
|
||||
|
||||
if (end != null) {
|
||||
options['end'] = end;
|
||||
}
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if (resp['logs']) {
|
||||
command.logs = resp['logs'];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function() {
|
||||
if (!$scope.currentBuild || $scope.polling) { return; }
|
||||
|
||||
$scope.polling = true;
|
||||
|
||||
var params = {
|
||||
|
@ -857,22 +955,25 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||
// root build object to remain the same object.
|
||||
$.extend(true, $scope.currentBuild, resp);
|
||||
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
|
||||
checkPollTimer();
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'commands': $scope.commands == null,
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if (resp['commands']) {
|
||||
$scope.commands = resp['commands'];
|
||||
}
|
||||
|
||||
processLogs(resp.logs, $scope.logStartIndex);
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if ($scope.logStartIndex != null && resp['start'] != $scope.logStartIndex) {
|
||||
$scope.polling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
processLogs(resp['logs'], resp['start']);
|
||||
$scope.logStartIndex = resp['total'];
|
||||
$scope.polling = false;
|
||||
}, function() {
|
||||
$scope.polling = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -881,8 +982,16 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
var params = {'repository': namespace + '/' + name};
|
||||
$rootScope.title = 'Loading Repository...';
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
if (!repo.can_write) {
|
||||
$rootScope.title = 'Unknown builds';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Builds';
|
||||
$scope.repo = repo;
|
||||
|
||||
getBuildInfo();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -897,13 +1006,12 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
} else if ($scope.builds.length > 0) {
|
||||
$scope.setCurrentBuild($scope.builds[0].id, true);
|
||||
$scope.setCurrentBuild($scope.builds[$scope.builds.length - 1].id, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchRepository();
|
||||
getBuildInfo();
|
||||
}
|
||||
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
|
||||
|
@ -914,8 +1022,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.logsShown = 0;
|
||||
$scope.deleting = false;
|
||||
|
||||
$scope.permissionCache = {};
|
||||
|
||||
$scope.buildEntityForPermission = function(name, permission, kind) {
|
||||
return {
|
||||
var key = name + ':' + kind;
|
||||
if ($scope.permissionCache[key]) {
|
||||
return $scope.permissionCache[key];
|
||||
}
|
||||
|
||||
return $scope.permissionCache[key] = {
|
||||
'kind': kind,
|
||||
'name': name,
|
||||
'is_robot': permission.is_robot,
|
||||
|
@ -936,7 +1051,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
|
||||
$scope.addNewPermission = function(entity) {
|
||||
// Don't allow duplicates.
|
||||
if ($scope.permissions[entity.kind][entity.name]) { return; }
|
||||
if (!entity || !entity.kind || $scope.permissions[entity.kind][entity.name]) { return; }
|
||||
|
||||
if (entity.is_org_member === false) {
|
||||
$scope.currentAddEntity = entity;
|
||||
|
@ -1156,8 +1271,13 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repo = repo;
|
||||
if (!repo.can_admin) {
|
||||
$rootScope.title = 'Forbidden';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.repo = repo;
|
||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
||||
': Permissions, webhooks and other settings';
|
||||
|
@ -1448,12 +1568,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
'initialize': false
|
||||
};
|
||||
|
||||
$('#couldnotbuildModal').on('hidden.bs.modal', function() {
|
||||
$scope.$apply(function() {
|
||||
$location.path('/repository/' + $scope.created.namespace + '/' + $scope.created.name);
|
||||
});
|
||||
});
|
||||
|
||||
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
||||
// accordingly.
|
||||
$scope.isUserNamespace = true;
|
||||
|
@ -1500,15 +1614,36 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
$scope.repo.namespace = namespace;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function() {
|
||||
var repo = $scope.repo;
|
||||
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||
};
|
||||
|
||||
$scope.handleBuildFailed = function(message) {
|
||||
var repo = $scope.repo;
|
||||
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Could not start Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary",
|
||||
"callback": function() {
|
||||
$scope.$apply(function() {
|
||||
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.createNewRepo = function() {
|
||||
$('#repoName').popover('hide');
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if ($scope.repo.initialize && uploader.files.length < 1) {
|
||||
$('#missingfileModal').modal();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.creating = true;
|
||||
var repo = $scope.repo;
|
||||
var data = {
|
||||
|
@ -1524,7 +1659,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
|
||||
// Repository created. Start the upload process if applicable.
|
||||
if ($scope.repo.initialize) {
|
||||
startFileUpload(created);
|
||||
$scope.createdForBuild = created;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1553,74 +1688,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
|||
|
||||
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
||||
};
|
||||
|
||||
var startBuild = function(repo, fileId) {
|
||||
$scope.building = true;
|
||||
|
||||
var data = {
|
||||
'file_id': fileId
|
||||
};
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$location.path('/repository/' + params.repository);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
|
||||
var conductUpload = function(repo, file, url, fileId, mimeType) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
$scope.uploading = false;
|
||||
startBuild(repo, fileId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var startFileUpload = function(repo) {
|
||||
$scope.uploading = true;
|
||||
$scope.uploading_progress = 0;
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
var file = uploader.files[0];
|
||||
$scope.upload_file = file.name;
|
||||
|
||||
var mimeType = file.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(repo, file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
};
|
||||
|
||||
var subscribedToPlan = function(sub) {
|
||||
$scope.planChanging = false;
|
||||
|
@ -1839,7 +1906,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
$rootScope.title = 'Loading...';
|
||||
|
||||
$scope.addNewMember = function(member) {
|
||||
if ($scope.members[member.name]) { return; }
|
||||
if (!member || $scope.members[member.name]) { return; }
|
||||
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
|
|
284
static/js/tour.js
Normal file
284
static/js/tour.js
Normal file
|
@ -0,0 +1,284 @@
|
|||
angular.module("angular-tour", [])
|
||||
.provider('AngularTour', function() {
|
||||
this.$get = ['$document', '$rootScope', '$compile', '$location', function($document, $rootScope, $compile, $location) {
|
||||
$rootScope.angular_tour_current = null;
|
||||
|
||||
function _start(tour, opt_stepIndex, opt_existingScope) {
|
||||
tour.initialStep = opt_stepIndex || tour.initialStep || 0;
|
||||
tour.tourScope = opt_existingScope || null;
|
||||
$rootScope.angular_tour_current = tour;
|
||||
}
|
||||
|
||||
function _stop() {
|
||||
$rootScope.angular_tour_current = null;
|
||||
}
|
||||
|
||||
return {
|
||||
start: _start,
|
||||
stop: _stop
|
||||
};
|
||||
}];
|
||||
})
|
||||
|
||||
.directive('angularTourUi', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/angular-tour-ui.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'tour': '=tour',
|
||||
'inline': '=inline',
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $location, $interval, AngularTour) {
|
||||
var createNewScope = function(initialScope) {
|
||||
var tourScope = jQuery.extend({}, initialScope || {});
|
||||
tourScope['_replaceData'] = function(s) {
|
||||
if (typeof s != 'string') {
|
||||
return s;
|
||||
}
|
||||
|
||||
for (var key in tourScope) {
|
||||
if (key[0] == '_') { continue; }
|
||||
if (tourScope.hasOwnProperty(key)) {
|
||||
s = s.replace('{{' + key + '}}', tourScope[key]);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
return tourScope;
|
||||
};
|
||||
|
||||
$scope.stepIndex = 0;
|
||||
$scope.step = null;
|
||||
$scope.interval = null;
|
||||
$scope.tourScope = null;
|
||||
|
||||
var getElement = function() {
|
||||
if (typeof $scope.step['element'] == 'function') {
|
||||
return $($scope.step['element'](tourScope));
|
||||
}
|
||||
|
||||
return $($scope.tourScope._replaceData($scope.step['element']));
|
||||
};
|
||||
|
||||
var checkSignal = function() {
|
||||
return $scope.step['signal'] && $scope.step['signal']($scope.tourScope);
|
||||
};
|
||||
|
||||
var teardownSignal = function() {
|
||||
if (!$scope.step) { return; }
|
||||
|
||||
var signal = $scope.step['signal'];
|
||||
if (signal && signal.$teardown) {
|
||||
signal.$teardown($scope.tourScope);
|
||||
}
|
||||
};
|
||||
|
||||
var setupSignal = function() {
|
||||
if (!$scope.step) { return; }
|
||||
|
||||
var signal = $scope.step['signal'];
|
||||
if (signal && signal.$setup) {
|
||||
signal.$setup($scope.tourScope);
|
||||
}
|
||||
};
|
||||
|
||||
var checkSignalTimer = function() {
|
||||
if (!$scope.step || !$scope.tourScope) {
|
||||
stopSignalTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkSignal()) {
|
||||
$scope.next();
|
||||
}
|
||||
};
|
||||
|
||||
var stopSignalTimer = function() {
|
||||
if (!$scope.interval) { return; }
|
||||
|
||||
$interval.cancel($scope.interval);
|
||||
$scope.interval = null;
|
||||
};
|
||||
|
||||
var startSignalTimer = function() {
|
||||
$scope.interval = $interval(checkSignalTimer, 500);
|
||||
};
|
||||
|
||||
var closeDomHighlight = function() {
|
||||
if (!$scope.step) { return; }
|
||||
|
||||
var element = getElement($scope.tourScope);
|
||||
element.spotlight('close');
|
||||
};
|
||||
|
||||
var updateDomHighlight = function() {
|
||||
var element = getElement();
|
||||
if (!element.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.spotlight({
|
||||
opacity: .5,
|
||||
speed: 400,
|
||||
color: '#333',
|
||||
animate: true,
|
||||
easing: 'linear',
|
||||
exitEvent: 'mouseenter',
|
||||
exitEventAppliesToElement: true,
|
||||
paddingX: 1,
|
||||
paddingY: 1
|
||||
});
|
||||
};
|
||||
|
||||
var fireMixpanelEvent = function() {
|
||||
if (!$scope.step || !mixpanel) { return; }
|
||||
|
||||
var eventName = $scope.step['mixpanelEvent'];
|
||||
if (eventName) {
|
||||
mixpanel.track(eventName);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setStepIndex = function(stepIndex) {
|
||||
// Close existing spotlight and signal timer.
|
||||
teardownSignal();
|
||||
closeDomHighlight();
|
||||
stopSignalTimer();
|
||||
|
||||
// Check if there is a next step.
|
||||
if (!$scope.tour || stepIndex >= $scope.tour.steps.length) {
|
||||
$scope.step = null;
|
||||
$scope.hasNextStep = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.step = $scope.tour.steps[stepIndex];
|
||||
|
||||
fireMixpanelEvent();
|
||||
|
||||
// If the signal is already true, then skip this step entirely.
|
||||
setupSignal();
|
||||
if (checkSignal()) {
|
||||
$scope.setStepIndex(stepIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.stepIndex = stepIndex;
|
||||
$scope.hasNextStep = stepIndex < $scope.tour.steps.length - 1;
|
||||
|
||||
// Need the timeout here to ensure the click event does not
|
||||
// hide the spotlight, and it has to be longer than the hide
|
||||
// spotlight animation timing.
|
||||
setTimeout(function() {
|
||||
updateDomHighlight();
|
||||
}, 500);
|
||||
|
||||
// Start listening for signals to move the tour forward.
|
||||
if ($scope.step.signal) {
|
||||
startSignalTimer();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.stop = function() {
|
||||
closeDomHighlight();
|
||||
$scope.tour = null;
|
||||
AngularTour.stop();
|
||||
};
|
||||
|
||||
$scope.next = function() {
|
||||
$scope.setStepIndex($scope.stepIndex + 1);
|
||||
};
|
||||
|
||||
$scope.$watch('tour', function(tour) {
|
||||
stopSignalTimer();
|
||||
if (tour) {
|
||||
// Set the tour scope.
|
||||
if (tour.tourScope) {
|
||||
$scope.tourScope = tour.tourScope;
|
||||
} else {
|
||||
$scope.tourScope = $scope.tour.tourScope = createNewScope(tour.initialScope);
|
||||
}
|
||||
|
||||
// Set the initial step.
|
||||
$scope.setStepIndex(tour.initialStep || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// If this is an inline tour, then we need to monitor the page to determine when
|
||||
// to transition it to an overlay tour.
|
||||
if ($scope.inline) {
|
||||
var counter = 0;
|
||||
var unbind = $rootScope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function(location) {
|
||||
// Since this callback fires for the first page display, we only unbind it
|
||||
// after the second call.
|
||||
if (counter == 1) {
|
||||
// Unbind the listener.
|
||||
unbind();
|
||||
|
||||
// Teardown any existing signal listener.
|
||||
teardownSignal();
|
||||
|
||||
// If there is an active tour, transition it over to the overlay.
|
||||
if ($scope.tour && $scope.step && $scope.step['overlayable']) {
|
||||
AngularTour.start($scope.tour, $scope.stepIndex + 1, $scope.tourScope);
|
||||
$scope.tour = null;
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.factory('AngularTourSignals', ['$location', function($location) {
|
||||
var signals = {};
|
||||
|
||||
// Signal: When the page location matches the given path.
|
||||
signals.matchesLocation = function(locationPath) {
|
||||
return function(tourScope) {
|
||||
return $location.path() == tourScope._replaceData(locationPath);
|
||||
};
|
||||
};
|
||||
|
||||
// Signal: When an element is found in the page's DOM.
|
||||
signals.elementAvaliable = function(elementPath) {
|
||||
return function(tourScope) {
|
||||
return $(tourScope._replaceData(elementPath)).length > 0;
|
||||
};
|
||||
};
|
||||
|
||||
// Signal: When a server-side event matches the predicate.
|
||||
signals.serverEvent = function(url, matcher) {
|
||||
var checker = function(tourScope) {
|
||||
return checker.$message && matcher(checker.$message, tourScope);
|
||||
};
|
||||
|
||||
checker.$message = null;
|
||||
|
||||
checker.$setup = function(tourScope) {
|
||||
var fullUrl = tourScope._replaceData(url);
|
||||
checker.$source = new EventSource(fullUrl);
|
||||
checker.$source.onmessage = function(e) {
|
||||
checker.$message = JSON.parse(e.data);
|
||||
};
|
||||
};
|
||||
|
||||
checker.$teardown = function() {
|
||||
if (checker.$source) {
|
||||
checker.$source.close();
|
||||
}
|
||||
};
|
||||
|
||||
return checker;
|
||||
};
|
||||
|
||||
return signals;
|
||||
}]);
|
335
static/lib/ansi2html.js
Normal file
335
static/lib/ansi2html.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
/**
|
||||
* Originally from: https://github.com/jorgeecardona/ansi-to-html
|
||||
* Modified by jschorr: Add ability to repeat existing styles and not close them out automatically.
|
||||
*/
|
||||
angular.module('ansiToHtml', []).value('ansi2html', (function() {
|
||||
// Define the styles supported from ANSI.
|
||||
var STYLES = {
|
||||
'ef0': 'color:#000',
|
||||
'ef1': 'color:#A00',
|
||||
'ef2': 'color:#0A0',
|
||||
'ef3': 'color:#A50',
|
||||
'ef4': 'color:#00A',
|
||||
'ef5': 'color:#A0A',
|
||||
'ef6': 'color:#0AA',
|
||||
'ef7': 'color:#AAA',
|
||||
'ef8': 'color:#555',
|
||||
'ef9': 'color:#F55',
|
||||
'ef10': 'color:#5F5',
|
||||
'ef11': 'color:#FF5',
|
||||
'ef12': 'color:#55F',
|
||||
'ef13': 'color:#F5F',
|
||||
'ef14': 'color:#5FF',
|
||||
'ef15': 'color:#FFF',
|
||||
'eb0': 'background-color:#000',
|
||||
'eb1': 'background-color:#A00',
|
||||
'eb2': 'background-color:#0A0',
|
||||
'eb3': 'background-color:#A50',
|
||||
'eb4': 'background-color:#00A',
|
||||
'eb5': 'background-color:#A0A',
|
||||
'eb6': 'background-color:#0AA',
|
||||
'eb7': 'background-color:#AAA',
|
||||
'eb8': 'background-color:#555',
|
||||
'eb9': 'background-color:#F55',
|
||||
'eb10': 'background-color:#5F5',
|
||||
'eb11': 'background-color:#FF5',
|
||||
'eb12': 'background-color:#55F',
|
||||
'eb13': 'background-color:#F5F',
|
||||
'eb14': 'background-color:#5FF',
|
||||
'eb15': 'background-color:#FFF'
|
||||
};
|
||||
|
||||
// Define the default styles.
|
||||
var DEFAULTS = {
|
||||
fg: '#FFF',
|
||||
bg: '#000'
|
||||
};
|
||||
|
||||
var __slice = [].slice;
|
||||
|
||||
var toHexString = function(num) {
|
||||
num = num.toString(16);
|
||||
while (num.length < 2) {
|
||||
num = "0" + num;
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
var escapeHtml = function (unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Define the derived styles.
|
||||
[0, 1, 2, 3, 4, 5].forEach(
|
||||
function(red) {
|
||||
return [0, 1, 2, 3, 4, 5].forEach(
|
||||
function(green) {
|
||||
return [0, 1, 2, 3, 4, 5].forEach(
|
||||
function(blue) {
|
||||
var b, c, g, n, r, rgb;
|
||||
c = 16 + (red * 36) + (green * 6) + blue;
|
||||
r = red > 0 ? red * 40 + 55 : 0;
|
||||
g = green > 0 ? green * 40 + 55 : 0;
|
||||
b = blue > 0 ? blue * 40 + 55 : 0;
|
||||
rgb = ((function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = [r, g, b];
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
n = _ref[_i];
|
||||
_results.push(toHexString(n));
|
||||
}
|
||||
return _results;
|
||||
})()).join('');
|
||||
STYLES["ef" + c] = "color:#" + rgb;
|
||||
return STYLES["eb" + c] = "background-color:#" + rgb;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
(function() {
|
||||
_results = [];
|
||||
for (_i = 0; _i <= 23; _i++){ _results.push(_i); }
|
||||
return _results;
|
||||
}).apply(this).forEach(
|
||||
function(gray) {
|
||||
var c, l;
|
||||
c = gray + 232;
|
||||
l = toHexString(gray * 10 + 8);
|
||||
STYLES["ef" + c] = "color:#" + l + l + l;
|
||||
return STYLES["eb" + c] = "background-color:#" + l + l + l;
|
||||
});
|
||||
|
||||
// Define the filter class which will track all the ANSI style state.
|
||||
function Filter(options) {
|
||||
this.opts = $.extend(true, DEFAULTS, options || {});
|
||||
this.input = [];
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
Filter.create = function() {
|
||||
return new Filter();
|
||||
};
|
||||
|
||||
Filter.prototype.toHtml = function(input) {
|
||||
this.resetStyles();
|
||||
var html = this.addInputToStream(input) + this.getTeardownHtml();
|
||||
this.resetStyles();
|
||||
return html;
|
||||
};
|
||||
|
||||
Filter.prototype.addInputToStream = function(input) {
|
||||
var buf = [];
|
||||
|
||||
this.input = typeof input === 'string' ? [input] : input;
|
||||
this.forEach(function(chunk) {
|
||||
return buf.push(chunk);
|
||||
});
|
||||
|
||||
return buf.join('');
|
||||
};
|
||||
|
||||
Filter.prototype.getSetupHtml = function() {
|
||||
return this.stack.map(function(data) {
|
||||
return data['html'];
|
||||
}).join('');
|
||||
};
|
||||
|
||||
Filter.prototype.getTeardownHtml = function() {
|
||||
var stackCopy = this.stack.slice();
|
||||
return stackCopy.reverse().map(function(data) {
|
||||
return "</" + data['kind'] + ">";
|
||||
}).join('');
|
||||
};
|
||||
|
||||
Filter.prototype.forEach = function(callback) {
|
||||
var that = this;
|
||||
var buf = '';
|
||||
var handleDisplay = function(code) {
|
||||
code = parseInt(code, 10);
|
||||
if (code === -1) {
|
||||
callback('<br/>');
|
||||
}
|
||||
if (code === 0) {
|
||||
callback(that.getTeardownHtml());
|
||||
that.resetStyles();
|
||||
}
|
||||
if (code === 1) {
|
||||
callback(that.pushTag('b'));
|
||||
}
|
||||
if (code === 2) {
|
||||
|
||||
}
|
||||
if ((2 < code && code < 5)) {
|
||||
callback(that.pushTag('u'));
|
||||
}
|
||||
if ((4 < code && code < 7)) {
|
||||
callback(that.pushTag('blink'));
|
||||
}
|
||||
if (code === 7) {
|
||||
|
||||
}
|
||||
if (code === 8) {
|
||||
callback(that.pushStyle('display:none'));
|
||||
}
|
||||
if (code === 9) {
|
||||
callback(that.pushTag('strike'));
|
||||
}
|
||||
if (code === 24) {
|
||||
callback(that.closeTag('u'));
|
||||
}
|
||||
if ((29 < code && code < 38)) {
|
||||
callback(that.pushStyle("ef" + (code - 30)));
|
||||
}
|
||||
if (code === 39) {
|
||||
callback(that.pushStyle("color:" + that.opts.fg));
|
||||
}
|
||||
if ((39 < code && code < 48)) {
|
||||
callback(that.pushStyle("eb" + (code - 40)));
|
||||
}
|
||||
if (code === 49) {
|
||||
callback(that.pushStyle("background-color:" + that.opts.bg));
|
||||
}
|
||||
if ((89 < code && code < 98)) {
|
||||
callback(that.pushStyle("ef" + (8 + (code - 90))));
|
||||
}
|
||||
if ((99 < code && code < 108)) {
|
||||
return callback(that.pushStyle("eb" + (8 + (code - 100))));
|
||||
}
|
||||
};
|
||||
|
||||
this.input.forEach(function(chunk) {
|
||||
buf += chunk;
|
||||
return that.tokenize(buf, function(tok, data) {
|
||||
switch (tok) {
|
||||
case 'text':
|
||||
return callback(escapeHtml(data));
|
||||
case 'display':
|
||||
return handleDisplay(data);
|
||||
case 'xterm256':
|
||||
return callback(that.pushStyle("ef" + data));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Filter.prototype.pushTag = function(tag, style) {
|
||||
if (style == null) {
|
||||
style = '';
|
||||
}
|
||||
if (style.length && style.indexOf(':') === -1) {
|
||||
style = STYLES[style];
|
||||
}
|
||||
var html = ["<" + tag, (style ? " style=\"" + style + "\"" : void 0), ">"].join('');
|
||||
this.stack.push({'html': html, 'kind': tag});
|
||||
return html;
|
||||
};
|
||||
|
||||
Filter.prototype.pushStyle = function(style) {
|
||||
return this.pushTag("span", style);
|
||||
};
|
||||
|
||||
Filter.prototype.closeTag = function(style) {
|
||||
var last;
|
||||
if (this.stack.slice(-1)[0]['kind'] === style) {
|
||||
last = this.stack.pop();
|
||||
}
|
||||
if (last != null) {
|
||||
return "</" + style + ">";
|
||||
}
|
||||
};
|
||||
|
||||
Filter.prototype.resetStyles = function() {
|
||||
this.stack = [];
|
||||
};
|
||||
|
||||
Filter.prototype.tokenize = function(text, callback) {
|
||||
var ansiHandler, ansiMatch, ansiMess, handler, i, length, newline, process, realText, remove, removeXterm256, tokens, _j, _len, _results1,
|
||||
_this = this;
|
||||
ansiMatch = false;
|
||||
ansiHandler = 3;
|
||||
remove = function(m) {
|
||||
return '';
|
||||
};
|
||||
removeXterm256 = function(m, g1) {
|
||||
callback('xterm256', g1);
|
||||
return '';
|
||||
};
|
||||
newline = function(m) {
|
||||
if (_this.opts.newline) {
|
||||
callback('display', -1);
|
||||
} else {
|
||||
callback('text', m);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
ansiMess = function(m, g1) {
|
||||
var code, _j, _len;
|
||||
ansiMatch = true;
|
||||
if (g1.trim().length === 0) {
|
||||
g1 = '0';
|
||||
}
|
||||
g1 = g1.trimRight(';').split(';');
|
||||
for (_j = 0, _len = g1.length; _j < _len; _j++) {
|
||||
code = g1[_j];
|
||||
callback('display', code);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
realText = function(m) {
|
||||
callback('text', m);
|
||||
return '';
|
||||
};
|
||||
tokens = [
|
||||
{
|
||||
pattern: /^\x08+/,
|
||||
sub: remove
|
||||
}, {
|
||||
pattern: /^\x1b\[38;5;(\d+)m/,
|
||||
sub: removeXterm256
|
||||
}, {
|
||||
pattern: /^\n+/,
|
||||
sub: newline
|
||||
}, {
|
||||
pattern: /^\x1b\[((?:\d{1,3};?)+|)m/,
|
||||
sub: ansiMess
|
||||
}, {
|
||||
pattern: /^\x1b\[?[\d;]{0,3}/,
|
||||
sub: remove
|
||||
}, {
|
||||
pattern: /^([^\x1b\x08\n]+)/,
|
||||
sub: realText
|
||||
}
|
||||
];
|
||||
process = function(handler, i) {
|
||||
var matches;
|
||||
if (i > ansiHandler && ansiMatch) {
|
||||
return;
|
||||
} else {
|
||||
ansiMatch = false;
|
||||
}
|
||||
matches = text.match(handler.pattern);
|
||||
text = text.replace(handler.pattern, handler.sub);
|
||||
};
|
||||
_results1 = [];
|
||||
while ((length = text.length) > 0) {
|
||||
for (i = _j = 0, _len = tokens.length; _j < _len; i = ++_j) {
|
||||
handler = tokens[i];
|
||||
process(handler, i);
|
||||
}
|
||||
if (text.length === length) {
|
||||
break;
|
||||
} else {
|
||||
_results1.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results1;
|
||||
};
|
||||
|
||||
return Filter;
|
||||
})());
|
197
static/lib/jquery.spotlight.js
Normal file
197
static/lib/jquery.spotlight.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* jQuery Spotlight
|
||||
*
|
||||
* Project Page: http://github.com/
|
||||
* Original Plugin code by Gilbert Pellegrom (2009)
|
||||
* Licensed under the GPL license (http://www.gnu.org/licenses/gpl-3.0.html)
|
||||
* Version 1.1 (2011)
|
||||
* Modified by jschorr (Fix Opacity bug, fix handling of events, add rounded corners)
|
||||
*/
|
||||
(function ($) {
|
||||
var currentOverlay;
|
||||
|
||||
$.fn.spotlight = function (options) {
|
||||
var method = 'create';
|
||||
|
||||
// Default settings
|
||||
settings = $.extend({}, {
|
||||
opacity: .5,
|
||||
speed: 400,
|
||||
color: '#333',
|
||||
animate: true,
|
||||
easing: '',
|
||||
exitEvent: 'click',
|
||||
exitEventAppliesToElement: false,
|
||||
onShow: function () {
|
||||
// do nothing
|
||||
},
|
||||
onHide: function () {
|
||||
// do nothing
|
||||
},
|
||||
spotlightZIndex: 9999,
|
||||
spotlightElementClass: 'spotlight-background',
|
||||
parentSelector: 'html',
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}, options);
|
||||
|
||||
function closeOverlay () {
|
||||
if (!currentOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.animate) {
|
||||
currentOverlay.animate({opacity: 0}, settings.speed, settings.easing, function () {
|
||||
if (currentOverlay != null) {
|
||||
currentOverlay.remove();
|
||||
currentOverlay = null;
|
||||
|
||||
// Trigger the onHide callback
|
||||
settings.onHide.call(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
currentOverlay.remove();
|
||||
currentOverlay = null;
|
||||
|
||||
// Trigger the onHide callback
|
||||
settings.onHide.call(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options === 'string') {
|
||||
method = options;
|
||||
options = arguments[1];
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'close':
|
||||
case 'destroy':
|
||||
closeOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
var elements = $(this),
|
||||
overlay,
|
||||
parent,
|
||||
context;
|
||||
|
||||
function roundRect(context, x, y, w, h, r) {
|
||||
if (w < 2 * r) r = w / 2;
|
||||
if (h < 2 * r) r = h / 2;
|
||||
context.beginPath();
|
||||
context.moveTo(x+r, y);
|
||||
context.arcTo(x+w, y, x+w, y+h, r);
|
||||
context.arcTo(x+w, y+h, x, y+h, r);
|
||||
context.arcTo(x, y+h, x, y, r);
|
||||
context.arcTo(x, y, x+w, y, r);
|
||||
context.closePath();
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colour in the overlay and clear all element masks
|
||||
*/
|
||||
function fillOverlay () {
|
||||
context.fillStyle = settings.color;
|
||||
context.fillRect(0, 0, parent.innerWidth(), parent.innerHeight());
|
||||
|
||||
// loop through elements and clear their position
|
||||
elements.each(function (i, e) {
|
||||
var ej = $(e);
|
||||
|
||||
var currentPos = e.getBoundingClientRect();
|
||||
context.save();
|
||||
context.globalCompositeOperation = 'destination-out';
|
||||
roundRect(context, currentPos.left - settings.paddingX,
|
||||
currentPos.top - settings.paddingY,
|
||||
ej.outerWidth() + (settings.paddingX * 2),
|
||||
ej.outerHeight() + (settings.paddingY * 2),
|
||||
6).fill();
|
||||
context.restore();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resizing the window
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
function handleResize (e) {
|
||||
overlay.attr('width', parent.innerWidth());
|
||||
overlay.attr('height', parent.innerHeight());
|
||||
|
||||
if (typeof context !== 'undefined') {
|
||||
fillOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
closeOverlay();
|
||||
|
||||
// Add the overlay element
|
||||
overlay = $('<canvas></canvas>');
|
||||
overlay.addClass(settings.spotlightElementClass);
|
||||
|
||||
currentOverlay = overlay;
|
||||
|
||||
parent = $(settings.parentSelector);
|
||||
parent.append(overlay);
|
||||
|
||||
// Get our elements
|
||||
var element = $(this);
|
||||
|
||||
// Set the CSS styles
|
||||
var cssConfig = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
zIndex: settings.spotlightZIndex,
|
||||
opacity: 0
|
||||
};
|
||||
|
||||
if (settings.parentSelector == 'html') {
|
||||
parent.css('height', '100%');
|
||||
}
|
||||
|
||||
overlay.css(cssConfig);
|
||||
handleResize();
|
||||
$(window).resize(handleResize);
|
||||
|
||||
context = overlay[0].getContext('2d');
|
||||
context.globalCompositeOperation = 'source-over';
|
||||
|
||||
fillOverlay();
|
||||
|
||||
// Fade in the spotlight
|
||||
if (settings.animate && jQuery.support.opacity) {
|
||||
overlay.animate({opacity: settings.opacity}, settings.speed, settings.easing, function () {
|
||||
// Trigger the onShow callback
|
||||
settings.onShow.call(this);
|
||||
});
|
||||
} else {
|
||||
if (jQuery.support.opacity) {
|
||||
overlay.css('opacity', settings.opacity);
|
||||
} else {
|
||||
overlay.css('filter', 'alpha(opacity=' + settings.opacity * 100 + ')');
|
||||
}
|
||||
// Trigger the onShow callback
|
||||
settings.onShow.call(this);
|
||||
}
|
||||
|
||||
// Set up click to close
|
||||
if (settings.exitEventAppliesToElement) {
|
||||
overlay.css({
|
||||
pointerEvents: 'none'
|
||||
});
|
||||
element.on(settings.exitEvent, overlay, closeOverlay);
|
||||
} else {
|
||||
$(document).on(settings.exitEvent, overlay, closeOverlay);
|
||||
}
|
||||
|
||||
// Returns the jQuery object to allow for chainability.
|
||||
return this;
|
||||
};
|
||||
|
||||
})(jQuery);
|
78
static/partials/about.html
Normal file
78
static/partials/about.html
Normal file
|
@ -0,0 +1,78 @@
|
|||
<div class="container about-us">
|
||||
<h2>
|
||||
About Us
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 about-basic-info">
|
||||
<h3>The Basics</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<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="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="about-basic-icon"><i class="fa fa-3x fa-users"></i></div>
|
||||
<div class="about-basic-text">
|
||||
<b> Worker Bees</b><br>
|
||||
2
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3>The Team</h3>
|
||||
Our team is composed of two software engineers turned entrepreneurs:
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-7 col-sm-offset-3 col-md-10 col-md-offset-2">
|
||||
<h4>Jacob Moshenko<br>
|
||||
<small>Co-Founder</small></h3>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/342ea83fd68d33f90b1f06f466d533c6?s=128&d=identicon">
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-10">
|
||||
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-7 col-sm-offset-3 col-md-10 col-md-offset-2">
|
||||
<h4>Joseph Schorr<br>
|
||||
<small>Co-Founder</small></h3>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<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>
|
||||
</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>
|
|
@ -5,7 +5,7 @@
|
|||
</h2>
|
||||
|
||||
<div class="row contact-options">
|
||||
<div class="col-sm-3 text-center">
|
||||
<div class="col-sm-3 text-center option-email">
|
||||
<span class="fa-stack fa-3x text-center">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-envelope fa-stack-1x fa-inverse"></i>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<h4><a href="mailto:support@quay.io">support@quay.io</a></h4>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 text-center">
|
||||
<div class="col-sm-3 text-center option-irc">
|
||||
<span class="fa-stack fa-3x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-comment fa-stack-1x fa-inverse"></i>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<h4><a href="irc://chat.freenode.net:6665/quayio">Freenode: #quayio</a></h4>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 text-center">
|
||||
<div class="col-sm-3 text-center option-phone">
|
||||
<span class="fa-stack fa-3x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-phone fa-stack-1x fa-inverse"></i>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<h4><a href="tel:+1-888-930-3475">888-930-3475</a></h4>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 text-center">
|
||||
<div class="col-sm-3 text-center option-twitter">
|
||||
<span class="fa-stack fa-3x">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div class="container repo repo-image-view">
|
||||
<div class="header">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
<h3>
|
||||
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
|
||||
<h3>
|
||||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo" image="image.value"></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
@ -54,7 +54,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-placement="right" data-container="body">
|
||||
<span class="description">This will also be the namespace for your repositories</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,16 +12,7 @@
|
|||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="container" ng-show="!user.anonymous && uploading">
|
||||
<span class="message">Uploading file {{ upload_file }}</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container new-repo" ng-show="!user.anonymous && !creating && !uploading && !building">
|
||||
<div class="container new-repo" ng-show="!user.anonymous && !creating && !building">
|
||||
<form method="post" name="newRepoForm" id="newRepoForm" ng-submit="createNewRepo()">
|
||||
|
||||
<!-- Header -->
|
||||
|
@ -109,11 +100,9 @@
|
|||
</div>
|
||||
|
||||
<div class="initialize-repo" ng-show="repo.initialize">
|
||||
<div class="init-description">
|
||||
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||
</div>
|
||||
|
||||
<input id="file-drop" class="file-drop" type="file">
|
||||
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
|
||||
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild"
|
||||
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,7 +112,7 @@
|
|||
<div class="col-md-1"></div>
|
||||
<div class="col-md-8">
|
||||
<button class="btn btn-large btn-success" type="submit"
|
||||
ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan))">
|
||||
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || (repo.initialize && !hasDockerfile)">
|
||||
Create Repository
|
||||
</button>
|
||||
</div>
|
||||
|
@ -157,26 +146,6 @@
|
|||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="missingfileModal">
|
||||
<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">File required</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A file is required in order to initialize a repository.
|
||||
</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="cannotcreateModal">
|
||||
<div class="modal-dialog">
|
||||
|
@ -195,26 +164,6 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="couldnotbuildModal">
|
||||
<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 initialize repository</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The repository could not be initialized with the selected Dockerfile. Please try again later.
|
||||
</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="couldnotsubscribeModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Organiaztion settings tab -->
|
||||
<!-- Organization settings tab -->
|
||||
<div id="settings" class="tab-pane">
|
||||
<div class="quay-spinner" ng-show="changingOrganization"></div>
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<div class="container" ng-show="deleting"><div class="quay-spinner"></div></div>
|
||||
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
|
||||
<div class="container repo repo-admin" ng-show="accessDenied">
|
||||
You do not have permission to view this page
|
||||
</div>
|
||||
<div class="container repo repo-admin" ng-show="repo && !deleting">
|
||||
<div class="header row">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
<h3>
|
||||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-cog'"
|
||||
subsection="'Admin'"></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
@ -88,8 +92,11 @@
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" class="admin-search">
|
||||
<span class="entity-search" namespace="repo.namespace" include-teams="true" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission" is-organization="repo.is_organization"></span>
|
||||
<td id="add-entity-permission" colspan="2" class="admin-search">
|
||||
<span class="entity-search" namespace="repo.namespace" include-teams="true"
|
||||
input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'"
|
||||
entity-selected="addNewPermission" is-organization="repo.is_organization"
|
||||
current-entity="selectedEntity"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,25 +1,35 @@
|
|||
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
|
||||
<div class="container repo repo-build" ng-show="repo && !deleting">
|
||||
<div class="container repo repo-build" ng-show="accessDenied">
|
||||
You do not have permission to view this page
|
||||
</div>
|
||||
<div class="container repo repo-build" ng-show="repo">
|
||||
<div class="header row">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||
<h3>
|
||||
<span class="repo-circle no-background" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-tasks'" subsection="'Build History'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="repo-controls">
|
||||
<button class="btn btn-success" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Dockerfile Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="!repo.is_building">
|
||||
There are no active builds for this repository
|
||||
<div class="row" ng-show="!builds.length">
|
||||
There are no builds for this repository
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="repo.is_building">
|
||||
<div class="row" ng-show="builds.length">
|
||||
<!-- Side tabs -->
|
||||
<div class="col-sm-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds">
|
||||
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds | reverse">
|
||||
<a class="build-tab-link" href="javascript:void(0)" ng-click="setCurrentBuild(build.id, true)">
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span>{{ getShortId(build.id) }}</span>
|
||||
<span>{{ build.display_name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -35,41 +45,76 @@
|
|||
Started: <span am-time-ago="build.started || 0"></span>
|
||||
</div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
<span class="build-message" build="build"></span>
|
||||
<span class="build-message" phase="build.phase"></span>
|
||||
<div class="build-progress" build="build"></div>
|
||||
</div>
|
||||
|
||||
<div class="build-logs">
|
||||
<div ng-show="!commands">
|
||||
<div ng-show="!logEntries">
|
||||
<span class="quay-spinner"></span>
|
||||
</div>
|
||||
<div class="command" ng-repeat="command in commands">
|
||||
<div class="command-entry" ng-click="toggleCommand(command)">
|
||||
<i class="fa chevron" ng-class="command.expanded ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
|
||||
<span bindonce>
|
||||
<span class="label" bo-class="getCommandKind(command.message)" bo-show="getCommandKind(command.message)"
|
||||
bo-text="getCommandKind(command.message)">
|
||||
</span>
|
||||
<span class="command-title" bo-html="getCommandTitleHtml(command.message)"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="command-logs panel-collapse collapse" ng-class="command.expanded ? 'in' : 'out'">
|
||||
<div class="log-entry" bindonce ng-repeat="log in command.logs">
|
||||
<span class="id" bo-text="$index + command.index + 1"></span>
|
||||
<span class="message" bo-text="log.message"></span>
|
||||
|
||||
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
|
||||
<div class="container-header" ng-class="container.type == 'phase' ? container.message : ''"
|
||||
ng-switch on="container.type" ng-click="toggleLogs(container)">
|
||||
<i class="fa chevron"
|
||||
ng-class="container.logs ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i>
|
||||
<div ng-switch-when="phase">
|
||||
<span class="container-content build-log-phase" phase="container"></span>
|
||||
</div>
|
||||
<div ng-show="!command.logs">
|
||||
<span class="quay-spinner"></span>
|
||||
<div ng-switch-when="error">
|
||||
<span class="container-content build-log-error" error="container"></span>
|
||||
</div>
|
||||
<div ng-switch-when="command">
|
||||
<span class="container-content build-log-command" command="container"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display the entries for the container -->
|
||||
<div class="container-logs" ng-show="container.logs">
|
||||
<div class="log-entry" bindonce ng-repeat="entry in container.logs">
|
||||
<span class="id" bo-text="$index + container.index + 1"></span>
|
||||
<span class="message" bo-html="processANSI(entry.message, container)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-top: 10px">
|
||||
<span class="quay-spinner" ng-show="polling"></span>
|
||||
<button class="btn" ng-show="(build.phase == 'error' || build.phase == 'complete') && build.resource_key"
|
||||
ng-class="build.phase == 'error' ? 'btn-success' : 'btn-default'"
|
||||
ng-click="askRestartBuild(build)">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Run Build Again
|
||||
</button>
|
||||
<span class="build-id">{{ build.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
|
||||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmRestartBuildModal">
|
||||
<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">Run Dockerfile Build?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to run this Dockerfile build again? The results will be immediately pushed to the repository.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="restartBuild(currentBuild)">Run Build</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,10 @@
|
|||
<div ng-show="user_repositories.value.length > 0">
|
||||
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
|
||||
<span class="repo-circle no-background" repo="repository"></span>
|
||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
|
||||
data-repo="{{repository.namespace}}/{{ repository.name }}">
|
||||
{{repository.namespace}}/{{repository.name}}
|
||||
</a>
|
||||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,7 +53,10 @@
|
|||
<div class="resource-view" resource="public_repositories">
|
||||
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
|
||||
<span class="repo-circle no-background" repo="repository"></span>
|
||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
|
||||
data-repo="{{repository.namespace}}/{{ repository.name }}">
|
||||
{{repository.namespace}}/{{repository.name}}
|
||||
</a>
|
||||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
<div class="page-controls">
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
<tr ng-show="canEditMembers">
|
||||
<td colspan="2">
|
||||
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
|
||||
entity-selected="addNewMember" is-organization="true"></span>
|
||||
entity-selected="addNewMember" is-organization="true"
|
||||
current-entity="selectedMember"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
3
static/partials/tutorial.html
Normal file
3
static/partials/tutorial.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="container">
|
||||
<div class="angular-tour-ui" tour="tour" inline="true"></div>
|
||||
</div>
|
|
@ -5,51 +5,65 @@
|
|||
</div>
|
||||
|
||||
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
|
||||
<div class="container repo">
|
||||
<div class="container repo repo-view">
|
||||
<!-- Repo Header -->
|
||||
<div class="header">
|
||||
<h3>
|
||||
<span class="repo-circle" repo="repo"></span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span class="repo-breadcrumb" repo="repo"></span>
|
||||
</h3>
|
||||
|
||||
<!-- Pull command -->
|
||||
<div class="pull-command visible-md visible-lg" style="display: none;">
|
||||
<span class="pull-command-title">Pull repository:</span>
|
||||
<div class="pull-container">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status boxes -->
|
||||
<div class="status-boxes">
|
||||
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building">
|
||||
<div class="dropdown" data-placement="top">
|
||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">Building Dockerfile<span ng-show="buildsInfo.length > 1">s</span> <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<div class="repo-controls">
|
||||
<!-- Builds -->
|
||||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||
bs-tooltip="buildsInfo ? 'Dockerfile Builds Running: ' + (buildsInfo.length) : 'Dockerfile Build'"
|
||||
ng-show="repo.can_write || buildsInfo.length">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-tasks fa-lg"></i>
|
||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '' }}</span></span>
|
||||
<b class="caret"></b>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-show="repo.can_write"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}">
|
||||
<i class="fa fa-tasks"></i>Dockerfile Build History</a>
|
||||
</li>
|
||||
<li ng-show="repo.can_write">
|
||||
<a href="javascript:void(0)" ng-click="showNewBuildDialog()">
|
||||
<i class="fa fa-plus" style="margin-left: 1px; margin-right: 8px;"></i>New Dockerfile Build
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="divider" ng-show="buildsInfo && repo.can_write"></li>
|
||||
<li role="presentation" class="dropdown-header" ng-show="buildsInfo">Current Builds</li>
|
||||
<li ng-repeat="buildInfo in buildsInfo">
|
||||
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="showBuild(buildInfo)">
|
||||
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="repo.can_write && showBuild(buildInfo)">
|
||||
<span class="build-status" build="buildInfo"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Admin -->
|
||||
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
|
||||
ng-show="repo.can_admin">
|
||||
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="top">
|
||||
<i class="fa fa-cog fa-lg"></i></button></a>
|
||||
|
||||
<!-- Pull Command -->
|
||||
<span class="pull-command visible-md-inline">
|
||||
<div class="pull-container" title="Pull repository" bs-tooltip="tooltip.title">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -72,7 +86,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
|||
|
||||
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
|
||||
<div class="empty-message">
|
||||
Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a>
|
||||
A build is currently processing. If this takes longer than an hour, please <a href="/contact">contact us</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -93,10 +107,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
|||
|
||||
<!-- Side Panel -->
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div id="side-panel" class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<!-- Dropdown -->
|
||||
<div class="tag-dropdown dropdown" data-placement="top">
|
||||
<div id="side-panel-dropdown" class="tag-dropdown dropdown" data-placement="top">
|
||||
<i class="fa fa-tag" ng-show="currentTag"></i>
|
||||
<i class="fa fa-archive" ng-show="!currentTag"></i>
|
||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
||||
|
@ -235,6 +249,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
|
||||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmdeleteTagModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -11,12 +11,20 @@
|
|||
</url>
|
||||
<url>
|
||||
<loc>https://quay.io/organizations/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<changefreq>weekly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://quay.io/repository/</loc>
|
||||
<changefreq>always</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://quay.io/contact/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://quay.io/about/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://quay.io/tos</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
|
|
24
static/tutorial/create-container.html
Normal file
24
static/tutorial/create-container.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<p>The first step to creating an image is to create a container and fill it with some data.</p>
|
||||
|
||||
<p>First we'll create a container with a single new file based off of the <code>ubuntu</code> base image:</p>
|
||||
|
||||
<pre class="command">
|
||||
<code ng-show="tour.tourScope.showSudo">sudo </code>docker run busybox echo "fun" > newfile
|
||||
</pre>
|
||||
|
||||
<p>The container will immediately terminate (because its one command is <code>echo</code>), so we'll use <code>docker ps -l</code> to list it:
|
||||
|
||||
<pre class="command">
|
||||
<code ng-show="tour.tourScope.showSudo">sudo </code>docker ps -l
|
||||
CONTAINER ID IMAGE COMMAND CREATED
|
||||
<var class="var1">07f2065197ef</var> ubuntu:12.04 echo fun 31 seconds ago
|
||||
</pre>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="containerId">Enter the container ID returned:</label>
|
||||
<div class="form-inline">
|
||||
<input type="text" id="containerId" class="form-control" placeholder="containerId" ng-model="tour.tourScope.containerId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
22
static/tutorial/create-image.html
Normal file
22
static/tutorial/create-image.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="alert alert-info">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="containerId">Enter a repository name:</label>
|
||||
<div class="form-inline">
|
||||
<form name="repoNameForm">
|
||||
<input type="text" id="repoName" name="repoName" class="form-control"
|
||||
ng-model="tour.tourScope.repoName" ng-pattern="/^[a-z0-9_-]+$/">
|
||||
</form>
|
||||
</div>
|
||||
<span ng-show="!repoNameForm.repoName.$valid" style="margin-left: 10px; margin-top: 7px;">
|
||||
Repository names must match [a-z0-9_-]+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Once a container has terminated in Docker, the next step is to <i>commit</i> the container to an image, and then <i>tag</i> that image with a relevant name so it can be saved to a repository.</p>
|
||||
|
||||
<p>Docker lets us do this in one step with the <i>commit</i> command. To do so, we run the <code>docker commit</code> with the container ID from the previous step and tag it to be a repository under <code>quay.io</code>.
|
||||
|
||||
<pre class="command">
|
||||
<code ng-show="tour.tourScope.showSudo">sudo </code>docker commit <var class="var1">{{ tour.tourScope.containerId || 'containerId' }}</var> quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
|
||||
</pre>
|
17
static/tutorial/docker-login.html
Normal file
17
static/tutorial/docker-login.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="container">
|
||||
<div class="alert alert-info" style="padding: 4px;">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input name="showSudo" type="checkbox" ng-model="tour.tourScope.showSudo" style="display: inline-block; margin-left: 0px; margin-right: 10px;">
|
||||
My OS requires me to run all <code>docker</code> commands with <code>sudo</code>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>The first step when using Quay.io is to login via the <code>docker login</code> command.</p>
|
||||
<p>Enter your Quay.io username and your password when prompted.</p>
|
||||
<pre class="command"><code ng-show="tour.tourScope.showSudo">sudo </code>docker login quay.io
|
||||
Username: {{ tour.tourScope.username }}
|
||||
Password: (password here)
|
||||
Email: {{ tour.tourScope.email }}</pre>
|
||||
</div>
|
1
static/tutorial/done.html
Normal file
1
static/tutorial/done.html
Normal file
|
@ -0,0 +1 @@
|
|||
That's it for the introduction tutorial! If you have any questions, please check the <a href="http://docs.quay.io" target="_blank">Quay Documentation</a> or <a href="/contact">contact us</a>!
|
2
static/tutorial/permissions.html
Normal file
2
static/tutorial/permissions.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<p>The permissions tab displays all the users, robot accounts and tokens that have access to the repository.</p>
|
||||
<p>By default, any repository you create will have your user ({{ tour.tourScope.username }}) as the single administrator.</p>
|
8
static/tutorial/push-image.html
Normal file
8
static/tutorial/push-image.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<p>Now that we've tagged our image with a repository name, we can <code>push</code> the repository to Quay.io:</p>
|
||||
|
||||
<pre class="command">
|
||||
<code ng-show="tour.tourScope.showSudo">sudo </code>docker push quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
|
||||
The push refers to a repository [quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>] (len: 1)
|
||||
Sending image list
|
||||
Pushing repository quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var> (1 tags)
|
||||
</pre>
|
1
static/tutorial/pushing.html
Normal file
1
static/tutorial/pushing.html
Normal file
|
@ -0,0 +1 @@
|
|||
Your repository <span class="repo-name-small">{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</span> is currently being pushed to Quay.io...
|
3
static/tutorial/repo-list.html
Normal file
3
static/tutorial/repo-list.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>This page displays all your personal repositories, as well as select public repositories</p>
|
||||
|
||||
<p>To view your new repository, click on <strong>{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</strong></p>
|
11
static/tutorial/signup.html
Normal file
11
static/tutorial/signup.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="container signin-container">
|
||||
<div class="row" style="margin-bottom: 10px">
|
||||
This tutorial will interact with your account, so please sign in to get started
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<div class="user-setup" redirect-url="'/tutorial/'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
3
static/tutorial/view-repo.html
Normal file
3
static/tutorial/view-repo.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>Your repository <span class="repo-name-small">{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</span> has been pushed to Quay.io!</p>
|
||||
|
||||
<p>To view your repository, first click on the <strong>Repositories</strong> link in the header to continue. This will display the repositories list.</p>
|
7
static/tutorial/waiting-repo-list.html
Normal file
7
static/tutorial/waiting-repo-list.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<p>
|
||||
Waiting for your repository to be listed.
|
||||
</p>
|
||||
|
||||
<p ng-show="tour.tourScope.inOrganization">
|
||||
Please make sure the <strong>{{ tour.tourScope.username }}</strong> namespace is selected.
|
||||
</p>
|
12
static/tutorial/welcome.html
Normal file
12
static/tutorial/welcome.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<p>This tutorial will walk you step-by-step through using Quay.io, covering:
|
||||
<ul>
|
||||
<li>Logging into Quay.io from Docker</li>
|
||||
<li>Starting a container</li>
|
||||
<li>Creating images from a container</li>
|
||||
<li>Pushing a repository to Quay.io</li>
|
||||
<li>Viewing a repository</li>
|
||||
<li>Changing a repository's permissions</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>This tutorial should take about <b>15 minutes</b>, but may take slightly longer depending on the speed of your internet connection</p>
|
|
@ -38,14 +38,12 @@
|
|||
|
||||
<script src="//code.jquery.com/jquery.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.0.0/bootbox.min.js"></script>
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-sanitize.min.js"></script>
|
||||
|
||||
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/restangular/1.2.0/restangular.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
|
||||
|
||||
<script src="static/lib/loading-bar.js"></script>
|
||||
<script src="static/lib/angular-strap.min.js"></script>
|
||||
|
@ -54,6 +52,7 @@
|
|||
<script src="static/lib/angulartics-google-analytics.js"></script>
|
||||
<script src="static/lib/angular-md5.js"></script>
|
||||
<script src="static/lib/bindonce.min.js"></script>
|
||||
<script src="static/lib/ansi2html.js"></script>
|
||||
|
||||
<script src="static/lib/angular-moment.min.js"></script>
|
||||
<script src="static/lib/angular-cookies.min.js"></script>
|
||||
|
@ -73,6 +72,7 @@
|
|||
window.__token = '{{ csrf_token() }}';
|
||||
</script>
|
||||
|
||||
<script src="static/js/tour.js"></script>
|
||||
<script src="static/js/app.js"></script>
|
||||
<script src="static/js/controllers.js"></script>
|
||||
<script src="static/js/graphing.js"></script>
|
||||
|
@ -117,10 +117,11 @@ var isProd = document.location.hostname === 'quay.io';
|
|||
<ul>
|
||||
<li><span class="copyright">©2014 DevTable, LLC</span></li>
|
||||
<li><a href="http://blog.devtable.com/">Blog</a></li>
|
||||
<li><a href="/tos" target="_self">Terms of Service</a></li>
|
||||
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
|
||||
<li><a href="/tos" target="_self">Terms</a></li>
|
||||
<li><a href="/privacy" target="_self">Privacy</a></li>
|
||||
<li><a href="/security/">Security</a></li>
|
||||
<li><b><a href="/contact/">Contact Us</a></b></li>
|
||||
<li><a href="/about/">About</a></li>
|
||||
<li><b><a href="/contact/">Contact</a></b></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -171,5 +172,6 @@ var isProd = document.location.hostname === 'quay.io';
|
|||
{% endif %}
|
||||
<!-- end olark code -->
|
||||
|
||||
<div class="angular-tour-ui" inline="false" tour="angular_tour_current"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
<script src="static/lib/Blob.js"></script>
|
||||
<script src="static/lib/FileSaver.js"></script>
|
||||
<script src="static/lib/jquery.base64.min.js"></script>
|
||||
<script src="static/lib/jquery.spotlight.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_content %}
|
||||
|
|
Binary file not shown.
|
@ -231,9 +231,9 @@ def build_specs():
|
|||
TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=403),
|
||||
TestSpec(url_for('api.get_webhook', repository=ORG_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=400),
|
||||
public_id=FAKE_WEBHOOK), admin_code=404),
|
||||
TestSpec(url_for('api.get_webhook', repository=PRIVATE_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=400),
|
||||
public_id=FAKE_WEBHOOK), admin_code=404),
|
||||
|
||||
TestSpec(url_for('api.list_webhooks', repository=PUBLIC_REPO),
|
||||
admin_code=403),
|
||||
|
@ -382,9 +382,9 @@ def build_specs():
|
|||
TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO,
|
||||
code=FAKE_TOKEN), admin_code=403),
|
||||
TestSpec(url_for('api.get_tokens', repository=ORG_REPO, code=FAKE_TOKEN),
|
||||
admin_code=400),
|
||||
admin_code=404),
|
||||
TestSpec(url_for('api.get_tokens', repository=PRIVATE_REPO,
|
||||
code=FAKE_TOKEN), admin_code=400),
|
||||
code=FAKE_TOKEN), admin_code=404),
|
||||
|
||||
TestSpec(url_for('api.create_token', repository=PUBLIC_REPO),
|
||||
admin_code=403).set_method('POST'),
|
||||
|
@ -587,13 +587,4 @@ def build_index_specs():
|
|||
IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO,
|
||||
tag=FAKE_TAG_NAME),
|
||||
NO_REPO, 403, 403, 403, 400).set_method('DELETE'),
|
||||
|
||||
IndexTestSpec(url_for('tags.delete_repository_tags',
|
||||
repository=PUBLIC_REPO),
|
||||
NO_REPO, 403, 403, 403, 403).set_method('DELETE'),
|
||||
IndexTestSpec(url_for('tags.delete_repository_tags',
|
||||
repository=PRIVATE_REPO),
|
||||
NO_REPO, 403, 403, 403, 204).set_method('DELETE'),
|
||||
IndexTestSpec(url_for('tags.delete_repository_tags', repository=ORG_REPO),
|
||||
NO_REPO, 403, 403, 403, 204).set_method('DELETE'),
|
||||
]
|
||||
|
|
1336
test/test_api_usage.py
Normal file
1336
test/test_api_usage.py
Normal file
File diff suppressed because it is too large
Load diff
265
test/testlogs.py
265
test/testlogs.py
|
@ -1,151 +1,194 @@
|
|||
import math
|
||||
import logging
|
||||
|
||||
from random import SystemRandom
|
||||
from loremipsum import get_sentence
|
||||
from functools import wraps
|
||||
from copy import deepcopy
|
||||
|
||||
from data.buildlogs import BuildLogs
|
||||
from random import choice
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
random = SystemRandom()
|
||||
|
||||
|
||||
def maybe_advance_script(is_get_status=False):
|
||||
def inner_advance(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
advance_units = random.randint(1, 500)
|
||||
logger.debug('Advancing script %s units', advance_units)
|
||||
while advance_units > 0 and self.remaining_script:
|
||||
units = self.remaining_script[0][0]
|
||||
|
||||
if advance_units > units:
|
||||
advance_units -= units
|
||||
self.advance_script(is_get_status)
|
||||
else:
|
||||
break
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
return inner_advance
|
||||
|
||||
|
||||
class TestBuildLogs(BuildLogs):
|
||||
COMMAND_TYPES = ['FROM', 'MAINTAINER', 'RUN', 'CMD', 'EXPOSE', 'ENV', 'ADD',
|
||||
'ENTRYPOINT', 'VOLUME', 'USER', 'WORKDIR']
|
||||
STATUS_TEMPLATE = {
|
||||
'total_commands': None,
|
||||
'current_command': None,
|
||||
'push_completion': 0.0,
|
||||
'image_completion': {},
|
||||
}
|
||||
|
||||
def __init__(self, redis_host, namespace, repository, test_build_id):
|
||||
super(TestBuildLogs, self).__init__(redis_host)
|
||||
self.namespace = namespace
|
||||
self.repository = repository
|
||||
self.test_build_id = test_build_id
|
||||
self.last_command = 0
|
||||
self.logs = [self._generate_command()]
|
||||
self.commands = [{
|
||||
'index': 0,
|
||||
'message': self.logs[0]['message'],
|
||||
}]
|
||||
self.request_counter = 0
|
||||
self._generate_logs()
|
||||
self.remaining_script = self._generate_script()
|
||||
logger.debug('Total script size: %s', len(self.remaining_script))
|
||||
self._logs = []
|
||||
|
||||
self._status = {}
|
||||
self._last_status = {}
|
||||
|
||||
def _get_random_command(self):
|
||||
COMMANDS = ['FROM', 'MAINTAINER', 'RUN', 'CMD', 'EXPOSE', 'ENV', 'ADD',
|
||||
'ENTRYPOINT', 'VOLUME', 'USER', 'WORKDIR']
|
||||
def advance_script(self, is_get_status):
|
||||
(_, log, status_wrapper) = self.remaining_script.pop(0)
|
||||
if log is not None:
|
||||
self._logs.append(log)
|
||||
|
||||
return choice(COMMANDS)
|
||||
if status_wrapper is not None:
|
||||
(phase, status) = status_wrapper
|
||||
|
||||
def _generate_command(self):
|
||||
self.last_command += 1
|
||||
from data import model
|
||||
build_obj = model.get_repository_build(self.namespace, self.repository,
|
||||
self.test_build_id)
|
||||
build_obj.phase = phase
|
||||
build_obj.save()
|
||||
|
||||
self._status = status
|
||||
if not is_get_status:
|
||||
self._last_status = status
|
||||
|
||||
def _generate_script(self):
|
||||
script = []
|
||||
|
||||
# generate the init phase
|
||||
script.append(self._generate_phase(400, 'initializing'))
|
||||
script.extend(self._generate_logs(random.randint(1, 3)))
|
||||
|
||||
# move to the building phase
|
||||
script.append(self._generate_phase(400, 'building'))
|
||||
total_commands = random.randint(5, 20)
|
||||
for command_num in range(1, total_commands + 1):
|
||||
command_weight = random.randint(50, 100)
|
||||
script.append(self._generate_command(command_num, total_commands,
|
||||
command_weight))
|
||||
|
||||
# we want 0 logs some percent of the time
|
||||
num_logs = max(0, random.randint(-50, 400))
|
||||
script.extend(self._generate_logs(num_logs))
|
||||
|
||||
# move to the pushing phase
|
||||
script.append(self._generate_phase(400, 'pushing'))
|
||||
script.extend(self._generate_push_statuses(total_commands))
|
||||
|
||||
# move to the error or complete phase
|
||||
if random.randint(0, 1) == 0:
|
||||
script.append(self._generate_phase(400, 'complete'))
|
||||
else:
|
||||
script.append(self._generate_phase(400, 'error'))
|
||||
script.append((1, {'message': 'Something bad happened! Oh noes!',
|
||||
'type': self.ERROR}, None))
|
||||
|
||||
return script
|
||||
|
||||
def _generate_phase(self, start_weight, phase_name):
|
||||
return (start_weight, {'message': phase_name, 'type': self.PHASE},
|
||||
(phase_name, deepcopy(self.STATUS_TEMPLATE)))
|
||||
|
||||
def _generate_command(self, command_num, total_commands, command_weight):
|
||||
sentence = get_sentence()
|
||||
command = self._get_random_command()
|
||||
command = random.choice(self.COMMAND_TYPES)
|
||||
if command == 'FROM':
|
||||
sentence = choice(['ubuntu', 'quay.io/devtable/simple', 'quay.io/buynlarge/orgrepo', 'stackbrew/ubuntu:precise'])
|
||||
sentence = random.choice(['ubuntu', 'lopter/raring-base',
|
||||
'quay.io/devtable/simple',
|
||||
'quay.io/buynlarge/orgrepo',
|
||||
'stackbrew/ubuntu:precise'])
|
||||
|
||||
return {
|
||||
'message': 'Step %s: %s %s' % (self.last_command, command, sentence),
|
||||
'is_command': True,
|
||||
msg = {
|
||||
'message': 'Step %s: %s %s' % (command_num, command, sentence),
|
||||
'type': self.COMMAND,
|
||||
}
|
||||
|
||||
def _generate_logs(self):
|
||||
rand = SystemRandom()
|
||||
num_logs = rand.randint(1, 500)
|
||||
for _ in range(num_logs):
|
||||
if rand.randint(1, 50) == 1:
|
||||
cmd = self._generate_command()
|
||||
self.commands.append({
|
||||
'message': cmd['message'],
|
||||
'index': len(self.logs),
|
||||
})
|
||||
self.logs.append(cmd)
|
||||
else:
|
||||
self.logs.append({
|
||||
'message': get_sentence(),
|
||||
})
|
||||
status = deepcopy(self.STATUS_TEMPLATE)
|
||||
status['total_commands'] = total_commands
|
||||
status['current_command'] = command_num
|
||||
return (command_weight, msg, ('building', status))
|
||||
|
||||
@staticmethod
|
||||
def _generate_image_completion(rand_func):
|
||||
images = {}
|
||||
for image_id in range(rand_func.randint(1, 11)):
|
||||
total = int(math.pow(abs(rand_func.gauss(0, 1000)), 2))
|
||||
current = rand_func.randint(0, total)
|
||||
image_id = 'image_id_%s' % image_id
|
||||
images[image_id] = {
|
||||
'total': total,
|
||||
'current': current,
|
||||
def _generate_logs(count):
|
||||
others = []
|
||||
if random.randint(0, 10) <= 8:
|
||||
count = count - 2
|
||||
others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)]
|
||||
|
||||
return others + [(1, {'message': get_sentence()}, None) for _ in range(count)]
|
||||
|
||||
@staticmethod
|
||||
def _compute_total_completion(statuses, total_images):
|
||||
percentage_with_sizes = float(len(statuses.values()))/total_images
|
||||
sent_bytes = sum([status[u'current'] for status in statuses.values()])
|
||||
total_bytes = sum([status[u'total'] for status in statuses.values()])
|
||||
return float(sent_bytes)/total_bytes*percentage_with_sizes
|
||||
|
||||
@staticmethod
|
||||
def _generate_push_statuses(total_commands):
|
||||
push_status_template = deepcopy(TestBuildLogs.STATUS_TEMPLATE)
|
||||
push_status_template['current_command'] = total_commands
|
||||
push_status_template['total_commands'] = total_commands
|
||||
|
||||
push_statuses = []
|
||||
|
||||
one_mb = 1 * 1024 * 1024
|
||||
|
||||
num_images = random.randint(2, 7)
|
||||
sizes = [random.randint(one_mb, one_mb * 5) for _ in range(num_images)]
|
||||
|
||||
image_completion = {}
|
||||
for image_num, image_size in enumerate(sizes):
|
||||
image_id = 'image_id_%s' % image_num
|
||||
|
||||
image_completion[image_id] = {
|
||||
'current': 0,
|
||||
'total': image_size,
|
||||
}
|
||||
return images
|
||||
|
||||
def _generate_fake_status(self):
|
||||
random = SystemRandom()
|
||||
phases = {
|
||||
'waiting': {},
|
||||
'starting': {
|
||||
'total_commands': 7,
|
||||
'current_command': 0,
|
||||
},
|
||||
'initializing': {},
|
||||
'error': {},
|
||||
'complete': {},
|
||||
'building': {
|
||||
'total_commands': 7,
|
||||
'current_command': random.randint(1, 7),
|
||||
},
|
||||
'pushing': {
|
||||
'total_commands': 7,
|
||||
'current_command': 7,
|
||||
'push_completion': random.random(),
|
||||
'image_completion': self._generate_image_completion(random),
|
||||
},
|
||||
}
|
||||
for i in range(one_mb, image_size, one_mb):
|
||||
image_completion[image_id]['current'] = i
|
||||
new_status = deepcopy(push_status_template)
|
||||
new_status['image_completion'] = deepcopy(image_completion)
|
||||
|
||||
phase = random.choice(phases.keys())
|
||||
completion = TestBuildLogs._compute_total_completion(image_completion,
|
||||
num_images)
|
||||
new_status['push_completion'] = completion
|
||||
push_statuses.append((250, None, ('pushing', new_status)))
|
||||
|
||||
from data import model
|
||||
build_obj = model.get_repository_build(self.namespace, self.repository,
|
||||
self.test_build_id)
|
||||
build_obj.phase = phase
|
||||
build_obj.save()
|
||||
return push_statuses
|
||||
|
||||
return phases[phase]
|
||||
|
||||
def get_log_entries(self, build_id, start_index, end_index):
|
||||
@maybe_advance_script()
|
||||
def get_log_entries(self, build_id, start_index):
|
||||
if build_id == self.test_build_id:
|
||||
self.request_counter += 1
|
||||
if self.request_counter % 10 == 0:
|
||||
self._generate_logs()
|
||||
logger.debug('Returning logs %s:%s', start_index, end_index)
|
||||
if end_index >= 0:
|
||||
end_index += 1
|
||||
return (len(self.logs), self.logs[start_index:end_index])
|
||||
return (len(self._logs), self._logs[start_index:])
|
||||
else:
|
||||
return super(TestBuildLogs, self).get_log_entries(build_id, start_index,
|
||||
end_index)
|
||||
|
||||
def get_commands(self, build_id):
|
||||
if build_id == self.test_build_id:
|
||||
self.request_counter += 1
|
||||
if self.request_counter % 10 == 0:
|
||||
self._generate_logs()
|
||||
return self.commands
|
||||
else:
|
||||
return super(TestBuildLogs, self).get_commands(build_id)
|
||||
|
||||
def get_last_command(self, build_id):
|
||||
if build_id == self.test_build_id:
|
||||
self.request_counter += 1
|
||||
if self.request_counter % 10 == 0:
|
||||
self._generate_logs()
|
||||
return self.commands[-1]
|
||||
else:
|
||||
return super(TestBuildLogs, self).get_last_command(build_id)
|
||||
return super(TestBuildLogs, self).get_log_entries(build_id, start_index)
|
||||
|
||||
@maybe_advance_script(True)
|
||||
def get_status(self, build_id):
|
||||
if build_id == self.test_build_id:
|
||||
self.request_counter += 1
|
||||
if self.request_counter % 10 == 0:
|
||||
self._generate_logs()
|
||||
last_status = self._status
|
||||
self._status = self._generate_fake_status()
|
||||
return last_status
|
||||
returnable_status = self._last_status
|
||||
self._last_status = self._status
|
||||
return returnable_status
|
||||
else:
|
||||
return super(TestBuildLogs, self).get_status(build_id)
|
||||
|
|
|
@ -35,3 +35,6 @@ class FakeUserfiles(object):
|
|||
|
||||
def get_file_url(self, file_id, expires_in=300):
|
||||
return ('http://fake/url')
|
||||
|
||||
def get_file_checksum(self, file_id):
|
||||
return 'abcdefg'
|
||||
|
|
48
tools/audittagimages.py
Normal file
48
tools/audittagimages.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from data.database import Image, RepositoryTag, Repository
|
||||
|
||||
from app import app
|
||||
|
||||
|
||||
store = app.config['STORAGE']
|
||||
|
||||
|
||||
tag_query = (RepositoryTag
|
||||
.select(RepositoryTag, Image, Repository)
|
||||
.join(Repository)
|
||||
.switch(RepositoryTag)
|
||||
.join(Image))
|
||||
|
||||
for tag in tag_query:
|
||||
if tag.image.repository.id != tag.repository.id:
|
||||
print('Repository tag pointing to external image: %s/%s:%s' %
|
||||
(tag.repository.namespace, tag.repository.name, tag.name))
|
||||
|
||||
proper_image_layer_path = store.image_layer_path(tag.repository.namespace,
|
||||
tag.repository.name,
|
||||
tag.image.docker_image_id)
|
||||
|
||||
has_storage = False
|
||||
if store.exists(proper_image_layer_path):
|
||||
print('Storage already in place: %s' % proper_image_layer_path)
|
||||
has_storage = True
|
||||
else:
|
||||
print('Storage missing: %s' % proper_image_layer_path)
|
||||
|
||||
has_db_entry = False
|
||||
new_image = None
|
||||
try:
|
||||
new_image = Image.get(Image.docker_image_id == tag.image.docker_image_id,
|
||||
Image.repository == tag.repository)
|
||||
has_db_entry = True
|
||||
print('DB image in place: %s invalid image id: %s' % (new_image.id,
|
||||
tag.image.id))
|
||||
except Image.DoesNotExist:
|
||||
print('DB image missing: %s' % tag.image.docker_image_id)
|
||||
|
||||
if has_storage and has_db_entry:
|
||||
print('Switching tag to proper image %s/%s/%s -> %s' %
|
||||
(tag.repository.namespace, tag.repository.name, tag.name,
|
||||
new_image.id))
|
||||
tag.image = new_image
|
||||
tag.save()
|
||||
print('Done')
|
39
workers/README.md
Normal file
39
workers/README.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
to prepare a new build node host:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core
|
||||
```
|
||||
|
||||
check out the code, install the kernel, custom docker, nsexec, and reboot:
|
||||
|
||||
```
|
||||
git clone https://bitbucket.org/yackob03/quay.git
|
||||
cd quay
|
||||
sudo gdebi --n binary_dependencies/builder/linux-headers-3.11.0-17_3.11.0-17.28_all.deb
|
||||
sudo gdebi --n binary_dependencies/builder/linux-headers-3.11.0-17-generic_3.11.0-17.28_amd64.deb
|
||||
sudo gdebi --n binary_dependencies/builder/linux-image-3.11.0-17-generic_3.11.0-17.28_amd64.deb
|
||||
sudo gdebi --n binary_dependencies/builder/linux-image-extra-3.11.0-17-generic_3.11.0-17.28_amd64.deb
|
||||
sudo gdebi --n binary_dependencies/builder/nsexec_1.22ubuntu1trusty1_amd64.deb
|
||||
sudo gdebi --n binary_dependencies/builder/lxc-docker-0.8.0-tutum_0.8.0-tutum-20140212002736-afad5c0-dirty_amd64.deb
|
||||
sudo chown -R 100000:100000 /var/lib/docker
|
||||
sudo shutdown -r now
|
||||
```
|
||||
|
||||
pull some base images if you want (optional)
|
||||
```
|
||||
sudo docker pull ubuntu
|
||||
sudo docker pull stackbrew/ubuntu
|
||||
sudo docker pull busybox
|
||||
sudo docker pull lopter/raring-base
|
||||
```
|
||||
|
||||
start the worker
|
||||
|
||||
```
|
||||
cd quay
|
||||
virtualenv --distribute venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
sudo STACK=prod venv/bin/python -m workers.dockerfilebuild -D
|
||||
```
|
|
@ -10,6 +10,7 @@ import shutil
|
|||
from docker import Client, APIError
|
||||
from tempfile import TemporaryFile, mkdtemp
|
||||
from zipfile import ZipFile
|
||||
from functools import partial
|
||||
|
||||
from data.queue import dockerfile_build_queue
|
||||
from data import model
|
||||
|
@ -53,9 +54,9 @@ class DockerfileBuildContext(object):
|
|||
self._build_dir = build_context_dir
|
||||
self._tag_name = tag_name
|
||||
self._push_token = push_token
|
||||
self._build_uuid = build_uuid
|
||||
self._cl = Client(timeout=1200, version='1.7')
|
||||
self._status = StatusWrapper(self._build_uuid)
|
||||
self._status = StatusWrapper(build_uuid)
|
||||
self._build_logger = partial(build_logs.append_log_message, build_uuid)
|
||||
|
||||
dockerfile_path = os.path.join(self._build_dir, "Dockerfile")
|
||||
self._num_steps = DockerfileBuildContext.__count_steps(dockerfile_path)
|
||||
|
@ -93,24 +94,25 @@ class DockerfileBuildContext(object):
|
|||
with self._status as status:
|
||||
status['total_commands'] = self._num_steps
|
||||
|
||||
logger.debug('Building to tag names: %s' % self._tag_name)
|
||||
logger.debug('Building to tag named: %s' % self._tag_name)
|
||||
build_status = self._cl.build(path=self._build_dir, tag=self._tag_name,
|
||||
stream=True)
|
||||
|
||||
current_step = 0
|
||||
built_image = None
|
||||
for status in build_status:
|
||||
logger.debug('Status: %s', str(status.encode('utf-8')))
|
||||
status_str = str(status.encode('utf-8'))
|
||||
logger.debug('Status: %s', status_str)
|
||||
step_increment = re.search(r'Step ([0-9]+) :', status)
|
||||
if step_increment:
|
||||
build_logs.append_command_message(str(status))
|
||||
self._build_logger(status_str, build_logs.COMMAND)
|
||||
current_step = int(step_increment.group(1))
|
||||
logger.debug('Step now: %s/%s' % (current_step, self._num_steps))
|
||||
with self._status as status:
|
||||
status['current_command'] = current_step
|
||||
continue
|
||||
else:
|
||||
build_logs.append_log_message(self._build_uuid, str(status))
|
||||
self._build_logger(status_str)
|
||||
|
||||
complete = re.match(r'Successfully built ([a-z0-9]+)$', status)
|
||||
if complete:
|
||||
|
@ -260,10 +262,15 @@ class DockerfileBuildWorker(Worker):
|
|||
tag_name = repository_build.tag
|
||||
access_token = repository_build.access_token.code
|
||||
|
||||
start_msg = ('Starting job with resource url: %s tag: %s and token: %s' %
|
||||
(resource_url, tag_name, access_token))
|
||||
log_appender = partial(build_logs.append_log_message,
|
||||
repository_build.uuid)
|
||||
|
||||
log_appender('initializing', build_logs.PHASE)
|
||||
|
||||
start_msg = ('Starting job with resource url: %s tag: %s' % (resource_url,
|
||||
tag_name))
|
||||
logger.debug(start_msg)
|
||||
build_logs.append_log_message(repository_build.uuid, start_msg)
|
||||
log_appender(start_msg)
|
||||
|
||||
docker_resource = requests.get(resource_url)
|
||||
c_type = docker_resource.headers['content-type']
|
||||
|
@ -271,13 +278,13 @@ class DockerfileBuildWorker(Worker):
|
|||
filetype_msg = ('Request to build file of type: %s with tag: %s' %
|
||||
(c_type, tag_name))
|
||||
logger.info(filetype_msg)
|
||||
build_logs.append_log_message(repository_build.uuid, filetype_msg)
|
||||
log_appender(filetype_msg)
|
||||
|
||||
if c_type not in self._mime_processors:
|
||||
raise RuntimeError('Invalid dockerfile content type: %s' % c_type)
|
||||
|
||||
build_dir = self._mime_processors[c_type](docker_resource)
|
||||
uuid = repository_build.uuid
|
||||
log_appender('building', build_logs.PHASE)
|
||||
repository_build.phase = 'building'
|
||||
repository_build.save()
|
||||
|
||||
|
@ -287,24 +294,28 @@ class DockerfileBuildWorker(Worker):
|
|||
built_image = build_ctxt.build()
|
||||
|
||||
if not built_image:
|
||||
log_appender('error', build_logs.PHASE)
|
||||
repository_build.phase = 'error'
|
||||
repository_build.save()
|
||||
build_logs.append_log_message(uuid, 'Unable to build dockerfile.')
|
||||
log_appender('Unable to build dockerfile.', build_logs.ERROR)
|
||||
return False
|
||||
|
||||
log_appender('pushing', build_logs.PHASE)
|
||||
repository_build.phase = 'pushing'
|
||||
repository_build.save()
|
||||
|
||||
build_ctxt.push(built_image)
|
||||
|
||||
log_appender('complete', build_logs.PHASE)
|
||||
repository_build.phase = 'complete'
|
||||
repository_build.save()
|
||||
|
||||
except Exception as exc:
|
||||
log_appender('error', build_logs.PHASE)
|
||||
logger.exception('Exception when processing request.')
|
||||
repository_build.phase = 'error'
|
||||
repository_build.save()
|
||||
build_logs.append_log_message(uuid, exc.message)
|
||||
log_appender(str(exc), build_logs.ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
Reference in a new issue