Merge branch 'master' into tutorial
Conflicts: endpoints/index.py static/css/quay.css static/js/app.js static/js/controllers.js test/data/test.db
This commit is contained in:
commit
98e57b9d2b
19 changed files with 1615 additions and 66 deletions
|
@ -4,7 +4,6 @@ import os
|
||||||
from app import app as application
|
from app import app as application
|
||||||
from data.model import db as model_db
|
from data.model import db as model_db
|
||||||
|
|
||||||
|
|
||||||
# Initialize logging
|
# Initialize logging
|
||||||
application.config['LOGGING_CONFIG']()
|
application.config['LOGGING_CONFIG']()
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from test import analytics as fake_analytics
|
||||||
|
|
||||||
class FlaskConfig(object):
|
class FlaskConfig(object):
|
||||||
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
|
||||||
|
JSONIFY_PRETTYPRINT_REGULAR = False
|
||||||
|
|
||||||
class FlaskProdConfig(FlaskConfig):
|
class FlaskProdConfig(FlaskConfig):
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
|
|
|
@ -109,7 +109,10 @@ def create_organization(name, email, creating_user):
|
||||||
|
|
||||||
return new_org
|
return new_org
|
||||||
except InvalidUsernameException:
|
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):
|
def create_robot(robot_shortname, parent):
|
||||||
|
@ -970,6 +973,41 @@ def delete_tag_and_images(namespace_name, repository_name, tag_name):
|
||||||
store.remove(repository_path)
|
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):
|
def get_tag_image(namespace_name, repository_name, tag_name):
|
||||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||||
fetched = list(joined.where(Repository.name == repository_name,
|
fetched = list(joined.where(Repository.name == repository_name,
|
||||||
|
@ -1021,7 +1059,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
(namespace_name, repository_name))
|
(namespace_name, repository_name))
|
||||||
|
|
||||||
try:
|
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:
|
except Image.DoesNotExist:
|
||||||
raise DataModelException('Invalid image with id: %s' %
|
raise DataModelException('Invalid image with id: %s' %
|
||||||
tag_docker_image_id)
|
tag_docker_image_id)
|
||||||
|
|
|
@ -262,7 +262,6 @@ def convert_user_to_organization():
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
def change_user_details():
|
def change_user_details():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
|
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -315,6 +314,8 @@ def create_new_user():
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
def signin_user():
|
def signin_user():
|
||||||
signin_data = request.get_json()
|
signin_data = request.get_json()
|
||||||
|
if not signin_data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
username = signin_data['username']
|
username = signin_data['username']
|
||||||
password = signin_data['password']
|
password = signin_data['password']
|
||||||
|
@ -421,6 +422,7 @@ def get_matching_entities(prefix):
|
||||||
|
|
||||||
team_data = [entity_team_view(team) for team in teams]
|
team_data = [entity_team_view(team) for team in teams]
|
||||||
user_data = [user_view(user) for user in users]
|
user_data = [user_view(user) for user in users]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'results': team_data + user_data
|
'results': team_data + user_data
|
||||||
})
|
})
|
||||||
|
@ -446,11 +448,16 @@ def create_organization():
|
||||||
existing = None
|
existing = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing = (model.get_organization(org_data['name']) or
|
existing = model.get_organization(org_data['name'])
|
||||||
model.get_user(org_data['name']))
|
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
try:
|
||||||
|
existing = model.get_user(org_data['name'])
|
||||||
|
except model.InvalidUserException:
|
||||||
|
pass
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
msg = 'A user or organization with this name already exists'
|
msg = 'A user or organization with this name already exists'
|
||||||
return request_error(message=msg)
|
return request_error(message=msg)
|
||||||
|
@ -604,9 +611,9 @@ def create_organization_prototype_permission(orgname):
|
||||||
'name' in details['activating_user']):
|
'name' in details['activating_user']):
|
||||||
activating_username = details['activating_user']['name']
|
activating_username = details['activating_user']['name']
|
||||||
|
|
||||||
delegate = details['delegate']
|
delegate = details['delegate'] if 'delegate' in details else {}
|
||||||
delegate_kind = delegate['kind']
|
delegate_kind = delegate.get('kind', None)
|
||||||
delegate_name = delegate['name']
|
delegate_name = delegate.get('name', None)
|
||||||
|
|
||||||
delegate_username = delegate_name if delegate_kind == 'user' else None
|
delegate_username = delegate_name if delegate_kind == 'user' else None
|
||||||
delegate_teamname = delegate_name if delegate_kind == 'team' 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')
|
return request_error(message='Unknown activating user')
|
||||||
|
|
||||||
if not delegate_user and not delegate_team:
|
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']
|
role_name = details['role']
|
||||||
|
|
||||||
|
@ -1278,7 +1285,11 @@ def create_webhook(namespace, repository):
|
||||||
def get_webhook(namespace, repository, public_id):
|
def get_webhook(namespace, repository, public_id):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
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))
|
return jsonify(webhook_view(webhook))
|
||||||
|
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
@ -1673,7 +1684,11 @@ def list_repo_tokens(namespace, repository):
|
||||||
def get_tokens(namespace, repository, code):
|
def get_tokens(namespace, repository, code):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
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))
|
return jsonify(token_view(perm))
|
||||||
|
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
@ -1810,6 +1825,8 @@ def set_card(user, token):
|
||||||
cus.save()
|
cus.save()
|
||||||
except stripe.CardError as e:
|
except stripe.CardError as e:
|
||||||
return carderror_response(e)
|
return carderror_response(e)
|
||||||
|
except stripe.InvalidRequestError as e:
|
||||||
|
return carderror_response(e)
|
||||||
|
|
||||||
return get_card(user)
|
return get_card(user)
|
||||||
|
|
||||||
|
|
|
@ -255,6 +255,8 @@ def update_images(namespace, repository):
|
||||||
event = app.config['USER_EVENTS'].get_event(username)
|
event = app.config['USER_EVENTS'].get_event(username)
|
||||||
event.publish_event_data('docker-cli', user_data)
|
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
|
# Generate a job for each webhook that has been added to this repo
|
||||||
webhooks = model.list_webhooks(namespace, repository)
|
webhooks = model.list_webhooks(namespace, repository)
|
||||||
for webhook in webhooks:
|
for webhook in webhooks:
|
||||||
|
@ -271,6 +273,7 @@ def update_images(namespace, repository):
|
||||||
'visibility': repo.visibility.name,
|
'visibility': repo.visibility.name,
|
||||||
'updated_tags': updated_tags,
|
'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))
|
webhook_queue.put(json.dumps(webhook_data))
|
||||||
|
|
||||||
|
|
|
@ -73,22 +73,8 @@ def delete_tag(namespace, repository, tag):
|
||||||
|
|
||||||
if permission.can():
|
if permission.can():
|
||||||
model.delete_tag(namespace, repository, tag)
|
model.delete_tag(namespace, repository, tag)
|
||||||
|
model.garbage_collect_repository(namespace, repository)
|
||||||
|
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 200)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
|
@ -94,6 +94,12 @@ def contact():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/about/')
|
||||||
|
@no_cache
|
||||||
|
def about():
|
||||||
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@web.route('/new/')
|
@web.route('/new/')
|
||||||
@no_cache
|
@no_cache
|
||||||
def new():
|
def new():
|
||||||
|
|
|
@ -2899,3 +2899,35 @@ pre.command:before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 10px;
|
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;
|
||||||
|
}
|
|
@ -427,6 +427,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
var planService = {};
|
var planService = {};
|
||||||
var listeners = [];
|
var listeners = [];
|
||||||
|
|
||||||
|
var previousSubscribeFailure = false;
|
||||||
|
|
||||||
planService.getFreePlan = function() {
|
planService.getFreePlan = function() {
|
||||||
return 'free';
|
return 'free';
|
||||||
};
|
};
|
||||||
|
@ -616,12 +618,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
||||||
|
|
||||||
planService.getCardInfo(orgname, function(cardInfo) {
|
planService.getCardInfo(orgname, function(cardInfo) {
|
||||||
if (plan.price > 0 && !cardInfo.last4) {
|
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
||||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousSubscribeFailure = false;
|
||||||
|
|
||||||
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
||||||
|
previousSubscribeFailure = true;
|
||||||
planService.handleCardError(resp);
|
planService.handleCardError(resp);
|
||||||
callbacks['failure'](resp);
|
callbacks['failure'](resp);
|
||||||
});
|
});
|
||||||
|
@ -784,11 +789,12 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
controller: TutorialCtrl}).
|
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',
|
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}).
|
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',
|
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
|
||||||
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||||
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
|
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
|
||||||
templateUrl: '/static/partials/security.html', controller: SecurityCtrl}).
|
templateUrl: '/static/partials/security.html'}).
|
||||||
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
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',
|
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}).
|
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
||||||
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
||||||
|
|
|
@ -15,8 +15,14 @@ $.fn.clipboardCopy = function() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function SigninCtrl($scope) {
|
function GuideCtrl() {
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function SecurityCtrl($scope) {
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactCtrl($scope) {
|
||||||
|
}
|
||||||
|
|
||||||
function PlansCtrl($scope, $location, UserService, PlanService) {
|
function PlansCtrl($scope, $location, UserService, PlanService) {
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
|
@ -42,9 +48,6 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function GuideCtrl($scope) {
|
|
||||||
}
|
|
||||||
|
|
||||||
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
|
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
|
||||||
$scope.tour = {
|
$scope.tour = {
|
||||||
'title': 'Quay.io Tutorial',
|
'title': 'Quay.io Tutorial',
|
||||||
|
@ -178,12 +181,6 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function SecurityCtrl($scope) {
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContactCtrl($scope) {
|
|
||||||
}
|
|
||||||
|
|
||||||
function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
|
function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
|
||||||
$scope.namespace = null;
|
$scope.namespace = null;
|
||||||
$scope.page = 1;
|
$scope.page = 1;
|
||||||
|
@ -756,6 +753,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
$scope.permissions = {'team': [], 'user': []};
|
$scope.permissions = {'team': [], 'user': []};
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
$scope.deleting = false;
|
$scope.deleting = false;
|
||||||
|
|
||||||
$scope.permissionCache = {};
|
$scope.permissionCache = {};
|
||||||
|
|
||||||
$scope.buildEntityForPermission = function(name, permission, kind) {
|
$scope.buildEntityForPermission = function(name, permission, kind) {
|
||||||
|
|
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>
|
</h2>
|
||||||
|
|
||||||
<div class="row contact-options">
|
<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">
|
<span class="fa-stack fa-3x text-center">
|
||||||
<i class="fa fa-circle fa-stack-2x"></i>
|
<i class="fa fa-circle fa-stack-2x"></i>
|
||||||
<i class="fa fa-envelope fa-stack-1x fa-inverse"></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>
|
<h4><a href="mailto:support@quay.io">support@quay.io</a></h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 text-center">
|
<div class="col-sm-3 text-center option-irc">
|
||||||
<span class="fa-stack fa-3x">
|
<span class="fa-stack fa-3x">
|
||||||
<i class="fa fa-circle fa-stack-2x"></i>
|
<i class="fa fa-circle fa-stack-2x"></i>
|
||||||
<i class="fa fa-comment fa-stack-1x fa-inverse"></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>
|
<h4><a href="irc://chat.freenode.net:6665/quayio">Freenode: #quayio</a></h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 text-center">
|
<div class="col-sm-3 text-center option-phone">
|
||||||
<span class="fa-stack fa-3x">
|
<span class="fa-stack fa-3x">
|
||||||
<i class="fa fa-circle fa-stack-2x"></i>
|
<i class="fa fa-circle fa-stack-2x"></i>
|
||||||
<i class="fa fa-phone fa-stack-1x fa-inverse"></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>
|
<h4><a href="tel:+1-888-930-3475">888-930-3475</a></h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 text-center">
|
<div class="col-sm-3 text-center option-twitter">
|
||||||
<span class="fa-stack fa-3x">
|
<span class="fa-stack fa-3x">
|
||||||
<i class="fa fa-circle fa-stack-2x"></i>
|
<i class="fa fa-circle fa-stack-2x"></i>
|
||||||
<i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
|
<i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<label for="orgName">Organization Name</label>
|
<label for="orgName">Organization Name</label>
|
||||||
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
||||||
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
||||||
data-placement="right">
|
data-placement="right" data-container="body">
|
||||||
<span class="description">This will also be the namespace for your repositories</span>
|
<span class="description">This will also be the namespace for your repositories</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,20 @@
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://quay.io/organizations/</loc>
|
<loc>https://quay.io/organizations/</loc>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://quay.io/repository/</loc>
|
<loc>https://quay.io/repository/</loc>
|
||||||
<changefreq>always</changefreq>
|
<changefreq>always</changefreq>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://quay.io/contact/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://quay.io/about/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://quay.io/tos</loc>
|
<loc>https://quay.io/tos</loc>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
|
|
|
@ -117,10 +117,11 @@ var isProd = document.location.hostname === 'quay.io';
|
||||||
<ul>
|
<ul>
|
||||||
<li><span class="copyright">©2014 DevTable, LLC</span></li>
|
<li><span class="copyright">©2014 DevTable, LLC</span></li>
|
||||||
<li><a href="http://blog.devtable.com/">Blog</a></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="/tos" target="_self">Terms</a></li>
|
||||||
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
|
<li><a href="/privacy" target="_self">Privacy</a></li>
|
||||||
<li><a href="/security/">Security</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -231,9 +231,9 @@ def build_specs():
|
||||||
TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO,
|
TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO,
|
||||||
public_id=FAKE_WEBHOOK), admin_code=403),
|
public_id=FAKE_WEBHOOK), admin_code=403),
|
||||||
TestSpec(url_for('api.get_webhook', repository=ORG_REPO,
|
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,
|
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),
|
TestSpec(url_for('api.list_webhooks', repository=PUBLIC_REPO),
|
||||||
admin_code=403),
|
admin_code=403),
|
||||||
|
@ -382,9 +382,9 @@ def build_specs():
|
||||||
TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO,
|
TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO,
|
||||||
code=FAKE_TOKEN), admin_code=403),
|
code=FAKE_TOKEN), admin_code=403),
|
||||||
TestSpec(url_for('api.get_tokens', repository=ORG_REPO, code=FAKE_TOKEN),
|
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,
|
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),
|
TestSpec(url_for('api.create_token', repository=PUBLIC_REPO),
|
||||||
admin_code=403).set_method('POST'),
|
admin_code=403).set_method('POST'),
|
||||||
|
@ -587,13 +587,4 @@ def build_index_specs():
|
||||||
IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO,
|
IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO,
|
||||||
tag=FAKE_TAG_NAME),
|
tag=FAKE_TAG_NAME),
|
||||||
NO_REPO, 403, 403, 403, 400).set_method('DELETE'),
|
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'),
|
|
||||||
]
|
]
|
||||||
|
|
1337
test/test_api_usage.py
Normal file
1337
test/test_api_usage.py
Normal file
File diff suppressed because it is too large
Load diff
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')
|
Reference in a new issue