Merge remote-tracking branch 'origin/master' into ncc1701
Conflicts: endpoints/web.py static/directives/signup-form.html static/js/app.js static/js/controllers.js static/partials/landing.html static/partials/view-repo.html test/data/test.db
This commit is contained in:
commit
0827e0fbac
45 changed files with 1149 additions and 306 deletions
|
@ -61,8 +61,10 @@ class InvalidBuildTriggerException(DataModelException):
|
|||
def create_user(username, password, email, is_organization=False):
|
||||
if not validate_email(email):
|
||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||
if not validate_username(username):
|
||||
raise InvalidUsernameException('Invalid username: %s' % username)
|
||||
|
||||
(username_valid, username_issue) = validate_username(username)
|
||||
if not username_valid:
|
||||
raise InvalidUsernameException('Invalid username %s: %s' % (username, username_issue))
|
||||
|
||||
# We allow password none for the federated login case.
|
||||
if password is not None and not validate_password(password):
|
||||
|
@ -125,9 +127,10 @@ def create_organization(name, email, creating_user):
|
|||
|
||||
|
||||
def create_robot(robot_shortname, parent):
|
||||
if not validate_username(robot_shortname):
|
||||
raise InvalidRobotException('The name for the robot \'%s\' is invalid.' %
|
||||
robot_shortname)
|
||||
(username_valid, username_issue) = validate_username(robot_shortname)
|
||||
if not username_valid:
|
||||
raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' %
|
||||
(robot_shortname, username_issue))
|
||||
|
||||
username = format_robot_username(parent.username, robot_shortname)
|
||||
|
||||
|
@ -214,8 +217,9 @@ def convert_user_to_organization(user, admin_user):
|
|||
|
||||
|
||||
def create_team(name, org, team_role_name, description=''):
|
||||
if not validate_username(name):
|
||||
raise InvalidTeamException('Invalid team name: %s' % name)
|
||||
(username_valid, username_issue) = validate_username(name)
|
||||
if not username_valid:
|
||||
raise InvalidTeamException('Invalid team name %s: %s' % (name, username_issue))
|
||||
|
||||
if not org.organization:
|
||||
raise InvalidOrganizationException('User with name %s is not an org.' %
|
||||
|
|
|
@ -16,8 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
|
|||
TriggerActivationException, EmptyRepositoryException,
|
||||
RepositoryReadException)
|
||||
from data import model
|
||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission
|
||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
||||
from util.names import parse_robot_username
|
||||
from util.dockerfileparse import parse_dockerfile
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -231,6 +232,141 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@internal_only
|
||||
class BuildTriggerAnalyze(RepositoryParamResource):
|
||||
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
||||
(such as a robot account to use for pulling)
|
||||
"""
|
||||
schemas = {
|
||||
'BuildTriggerAnalyzeRequest': {
|
||||
'id': 'BuildTriggerAnalyzeRequest',
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'config'
|
||||
],
|
||||
'properties': {
|
||||
'config': {
|
||||
'type': 'object',
|
||||
'description': 'Arbitrary json.',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('analyzeBuildTrigger')
|
||||
@validate_json_request('BuildTriggerAnalyzeRequest')
|
||||
def post(self, namespace, repository, trigger_uuid):
|
||||
""" Analyze the specified build trigger configuration. """
|
||||
try:
|
||||
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||
except model.InvalidBuildTriggerException:
|
||||
raise NotFound()
|
||||
|
||||
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
|
||||
new_config_dict = request.get_json()['config']
|
||||
|
||||
try:
|
||||
# Load the contents of the Dockerfile.
|
||||
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict)
|
||||
if not contents:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not read the Dockerfile for the trigger'
|
||||
}
|
||||
|
||||
# Parse the contents of the Dockerfile.
|
||||
parsed = parse_dockerfile(contents)
|
||||
if not parsed:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not parse the Dockerfile specified'
|
||||
}
|
||||
|
||||
# Determine the base image (i.e. the FROM) for the Dockerfile.
|
||||
base_image = parsed.get_base_image()
|
||||
if not base_image:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': 'No FROM line found in the Dockerfile'
|
||||
}
|
||||
|
||||
# Check to see if the base image lives in Quay.
|
||||
quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME'])
|
||||
|
||||
if not base_image.startswith(quay_registry_prefix):
|
||||
return {
|
||||
'status': 'publicbase'
|
||||
}
|
||||
|
||||
# Lookup the repository in Quay.
|
||||
result = base_image[len(quay_registry_prefix):].split('/', 2)
|
||||
if len(result) != 2:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': '"%s" is not a valid Quay repository path' % (base_image)
|
||||
}
|
||||
|
||||
(base_namespace, base_repository) = result
|
||||
found_repository = model.get_repository(base_namespace, base_repository)
|
||||
if not found_repository:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# If the repository is private and the user cannot see that repo, then
|
||||
# mark it as not found.
|
||||
can_read = ReadRepositoryPermission(base_namespace, base_repository)
|
||||
if found_repository.visibility.name != 'public' and not can_read:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# Check to see if the repository is public. If not, we suggest the
|
||||
# usage of a robot account to conduct the pull.
|
||||
read_robots = []
|
||||
|
||||
if AdministerOrganizationPermission(base_namespace).can():
|
||||
def robot_view(robot):
|
||||
return {
|
||||
'name': robot.username,
|
||||
'kind': 'user',
|
||||
'is_robot': True
|
||||
}
|
||||
|
||||
def is_valid_robot(user):
|
||||
# Make sure the user is a robot.
|
||||
if not user.robot:
|
||||
return False
|
||||
|
||||
# Make sure the current user can see/administer the robot.
|
||||
(robot_namespace, shortname) = parse_robot_username(user.username)
|
||||
return AdministerOrganizationPermission(robot_namespace).can()
|
||||
|
||||
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
|
||||
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
|
||||
|
||||
return {
|
||||
'namespace': base_namespace,
|
||||
'name': base_repository,
|
||||
'is_public': found_repository.visibility.name == 'public',
|
||||
'robots': read_robots,
|
||||
'status': 'analyzed',
|
||||
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict)
|
||||
}
|
||||
|
||||
except RepositoryReadException as rre:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': rre.message
|
||||
}
|
||||
|
||||
raise NotFound()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
""" Custom verb to manually activate a build trigger. """
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
import io
|
||||
import os.path
|
||||
import tarfile
|
||||
import base64
|
||||
|
||||
from github import Github, UnknownObjectException, GithubException
|
||||
from tempfile import SpooledTemporaryFile
|
||||
|
@ -45,6 +46,19 @@ class BuildTrigger(object):
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
"""
|
||||
Returns the URL at which the Dockerfile for the trigger can be found or None if none/not applicable.
|
||||
"""
|
||||
return None
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
"""
|
||||
Loads the Dockerfile found for the trigger's config and returns them or None if none could
|
||||
be found/loaded.
|
||||
"""
|
||||
return None
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
"""
|
||||
Take the auth information for the specific trigger type and load the
|
||||
|
@ -167,7 +181,6 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
return config
|
||||
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
gh_client = self._get_client(auth_token)
|
||||
usr = gh_client.get_user()
|
||||
|
@ -218,6 +231,41 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
source = config['build_source']
|
||||
subdirectory = config.get('subdir', '')
|
||||
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
|
||||
|
||||
gh_client = self._get_client(auth_token)
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
master_branch = repo.master_branch or 'master'
|
||||
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
|
||||
except GithubException as ge:
|
||||
return None
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
gh_client = self._get_client(auth_token)
|
||||
|
||||
source = config['build_source']
|
||||
subdirectory = config.get('subdir', '')
|
||||
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
|
||||
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
file_info = repo.get_file_contents(path)
|
||||
if file_info is None:
|
||||
return None
|
||||
|
||||
content = file_info.content
|
||||
if file_info.encoding == 'base64':
|
||||
content = base64.b64decode(content)
|
||||
return content
|
||||
|
||||
except GithubException as ge:
|
||||
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
||||
# Prepare the download and upload URLs
|
||||
|
|
|
@ -9,6 +9,7 @@ from urlparse import urlparse
|
|||
from data import model
|
||||
from data.model.oauth import DatabaseAuthorizationProvider
|
||||
from app import app, billing as stripe
|
||||
from auth.auth import require_session_login
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
from util.seo import render_snapshot
|
||||
|
@ -161,6 +162,7 @@ def privacy():
|
|||
|
||||
@web.route('/receipt', methods=['GET'])
|
||||
@route_show_if(features.BILLING)
|
||||
@require_session_login
|
||||
def receipt():
|
||||
if not current_user.is_authenticated():
|
||||
abort(401)
|
||||
|
|
|
@ -292,7 +292,7 @@ def populate_database():
|
|||
|
||||
__generate_repository(new_user_1, 'complex',
|
||||
'Complex repository with many branches and tags.',
|
||||
False, [(new_user_2, 'read')],
|
||||
False, [(new_user_2, 'read'), (dtrobot[0], 'read')],
|
||||
(2, [(3, [], 'v2.0'),
|
||||
(1, [(1, [(1, [], ['prod'])],
|
||||
'staging'),
|
||||
|
|
|
@ -2406,12 +2406,19 @@ p.editable:hover i {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#image-history-container .tags .tag, #confirmdeleteTagModal .tag {
|
||||
.tags .tag, #confirmdeleteTagModal .tag {
|
||||
border-radius: 10px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip-tags {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
border-top: 1px dotted #aaa;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#changes-tree-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -3451,7 +3458,7 @@ pre.command:before {
|
|||
|
||||
position: relative;
|
||||
|
||||
height: 100px;
|
||||
height: 75px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -3602,10 +3609,10 @@ pre.command:before {
|
|||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.trigger-option-section:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
.trigger-option-section:not(:first-child) {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.trigger-option-section .entity-search-element .twitter-typeahead {
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="id-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||
<span class="input-group-addon" title="Copy to Clipboard">
|
||||
<span class="input-group-addon" data-title="Copy to Clipboard">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<span class="delete-ui-element" ng-click="focus()">
|
||||
<span class="delete-ui-button" ng-click="performDelete()"><button class="btn btn-danger">{{ buttonTitleInternal }}</button></span>
|
||||
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" title="{{ deleteTitle }}"></i>
|
||||
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" data-title="{{ deleteTitle }}"></i>
|
||||
</span>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<span class="entity-reference-element">
|
||||
<span ng-if="entity.kind == 'team'">
|
||||
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-group" data-title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<span class="entity-name">
|
||||
<span ng-if="!getIsAdmin(namespace)">{{entity.name}}</span>
|
||||
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="entity.kind != 'team'">
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<span class="entity-name" ng-if="entity.is_robot">
|
||||
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
||||
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
||||
|
@ -22,6 +22,6 @@
|
|||
</span>
|
||||
</span>
|
||||
<i class="fa fa-exclamation-triangle" ng-if="entity.is_org_member === false"
|
||||
title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
|
||||
data-title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
|
||||
</i>
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntity" entity="currentEntity"></span>
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntityInternal" entity="currentEntityInternal"></span>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
||||
ng-click="lazyLoad()">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</form>
|
||||
|
||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
|
||||
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" data-title="Create new repository"></i></a>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
|||
ng-show="notificationService.notifications.length"
|
||||
ng-class="notificationService.notificationClasses"
|
||||
bs-tooltip=""
|
||||
title="User Notifications"
|
||||
data-title="User Notifications"
|
||||
data-placement="left"
|
||||
data-container="body">
|
||||
{{ notificationService.notifications.length }}
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
</span>
|
||||
<span class="right">
|
||||
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
||||
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
<a href="{{ logsPath }}" download="usage-log.json" target="_new">
|
||||
<i class="fa fa-download toggle-icon" title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-download toggle-icon" data-title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -55,7 +55,7 @@
|
|||
<td>
|
||||
<span class="log-performer" ng-if="log.metadata.oauth_token_application">
|
||||
<div>
|
||||
<span class="application-reference" title="log.metadata.oauth_token_application"
|
||||
<span class="application-reference" data-title="log.metadata.oauth_token_application"
|
||||
client-id="log.metadata.oauth_token_application_id"></span>
|
||||
</div>
|
||||
<div style="text-align: center; font-size: 12px; color: #aaa; padding: 4px;">on behalf of</div>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</a>
|
||||
|
||||
<i class="fa fa-exclamation-triangle" ng-show="requireCreate && !namespaces[org.name].can_create_repo"
|
||||
title="You do not have permission to create repositories for this organization"
|
||||
data-title="You do not have permission to create repositories for this organization"
|
||||
data-placement="right"
|
||||
bs-tooltip="tooltip.title"></i>
|
||||
</li>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<td>
|
||||
{{ plan.title }}
|
||||
<div class="deprecated-plan-label" ng-show="plan.deprecated">
|
||||
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||
<span class="context-tooltip" data-title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ plan.privateRepos }}</td>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<div class="container" ng-show="!loading">
|
||||
<div class="alert alert-info">
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
|
||||
</div>
|
||||
|
||||
<div class="side-controls">
|
||||
|
@ -17,13 +17,13 @@
|
|||
<thead>
|
||||
<th>
|
||||
<span class="context-tooltip"
|
||||
title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
|
||||
data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Repository Creator
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="context-tooltip" title="The user, robot or team that is being granted the permission"
|
||||
<span class="context-tooltip" data-title="The user, robot or team that is being granted the permission"
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Applies To User/Robot/Team
|
||||
</span>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" title="Private Repository"></i>
|
||||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" data-title="Private Repository"></i>
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
|
|
100
static/directives/setup-trigger-dialog.html
Normal file
100
static/directives/setup-trigger-dialog.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
<div class="setup-trigger-directive-element">
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<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">Setup new build trigger</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Trigger-specific setup -->
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repository" trigger="trigger"
|
||||
analyze="checkAnalyze(isValid)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pull information -->
|
||||
<div class="trigger-option-section" ng-show="showPullRequirements">
|
||||
<div ng-show="!pullRequirements">
|
||||
<span class="quay-spinner"></span> Checking pull credential requirements...
|
||||
</div>
|
||||
|
||||
<div ng-show="pullRequirements">
|
||||
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false">
|
||||
The
|
||||
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
|
||||
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
|
||||
depends on repository
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
|
||||
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
||||
</a> which requires
|
||||
a robot account for pull access, because it is marked <strong>private</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table style="width: 100%;" ng-show="pullRequirements">
|
||||
<tr>
|
||||
<td style="width: 114px">
|
||||
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
|
||||
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repository.namespace }}</strong>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">Public</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)">
|
||||
<i class="fa fa-wrench"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="!publicPull">
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div class="entity-search" namespace="repository.namespace" include-teams="false"
|
||||
input-title="'Select robot account for pulling...'"
|
||||
is-organization="repository.is_organization"
|
||||
is-persistent="true"
|
||||
current-entity="pullEntity"
|
||||
filter="['robot']"></div>
|
||||
|
||||
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the repository.
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-if="!pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||
Note: No robot account currently has access to the repository. Please create one and/or assign access in the
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
|
||||
ng-click="activate">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
|
@ -1,13 +1,15 @@
|
|||
<div class="signup-form-element">
|
||||
<form class="form-signup" name="signupForm" ng-submit="register()" data-trigger="manual"
|
||||
data-content="{{ registerError }}" data-placement="left" ng-show="!awaitingConfirmation && !registering">
|
||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required>
|
||||
<form class="form-signup" name="signupForm" ng-submit="register()" ngshow="!awaitingConfirmation && !registering">
|
||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
|
||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword"
|
||||
match="newUser.password" required>
|
||||
match="newUser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
<div class="form-group signin-buttons">
|
||||
<button class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||
<button id="signupButton"
|
||||
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||
analytics-on analytics-event="register">
|
||||
<span quay-show="Features.BILLING">Sign Up for Free!</span>
|
||||
<span quay-show="!Features.BILLING">Sign Up</span>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<span class="trigger-description-element" ng-switch on="trigger.service">
|
||||
<span ng-switch-when="github">
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px" title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a>
|
||||
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="trigger.config.subdir">
|
||||
<span>Dockerfile:
|
||||
|
|
233
static/js/app.js
233
static/js/app.js
|
@ -335,6 +335,42 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
}]);
|
||||
|
||||
|
||||
$provide.factory('UIService', [function() {
|
||||
var uiService = {};
|
||||
|
||||
uiService.hidePopover = function(elem) {
|
||||
var popover = $('#signupButton').data('bs.popover');
|
||||
if (popover) {
|
||||
popover.hide();
|
||||
}
|
||||
};
|
||||
|
||||
uiService.showPopover = function(elem, content) {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
if (!popover) {
|
||||
$(elem).popover({'content': '-', 'placement': 'left'});
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
popover.options.content = content;
|
||||
popover.show();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
uiService.showFormError = function(elem, result) {
|
||||
var message = result.data['message'] || result.data['error_description'] || '';
|
||||
if (message) {
|
||||
uiService.showPopover(elem, message);
|
||||
} else {
|
||||
uiService.hidePopover(elem);
|
||||
}
|
||||
};
|
||||
|
||||
return uiService;
|
||||
}]);
|
||||
|
||||
|
||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||
var utilService = {};
|
||||
|
||||
|
@ -1833,7 +1869,7 @@ quayApp.directive('signupForm', function () {
|
|||
scope: {
|
||||
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
||||
$('.form-signup').popover();
|
||||
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
|
@ -1847,24 +1883,21 @@ quayApp.directive('signupForm', function () {
|
|||
|
||||
$scope.awaitingConfirmation = false;
|
||||
$scope.registering = false;
|
||||
|
||||
|
||||
$scope.register = function() {
|
||||
$('.form-signup').popover('hide');
|
||||
UIService.hidePopover('#signupButton');
|
||||
$scope.registering = true;
|
||||
|
||||
ApiService.createNewUser($scope.newUser).then(function() {
|
||||
$scope.awaitingConfirmation = true;
|
||||
$scope.registering = false;
|
||||
$scope.awaitingConfirmation = true;
|
||||
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.alias($scope.newUser.username);
|
||||
}
|
||||
}, function(result) {
|
||||
$scope.registering = false;
|
||||
$scope.registerError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('.form-signup').popover('show');
|
||||
});
|
||||
UIService.showFormError('#signupButton', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -2904,6 +2937,7 @@ quayApp.directive('entitySearch', function () {
|
|||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
||||
$scope.lazyLoading = true;
|
||||
$scope.isAdmin = false;
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||
|
@ -2986,7 +3020,9 @@ quayApp.directive('entitySearch', function () {
|
|||
};
|
||||
|
||||
$scope.clearEntityInternal = function() {
|
||||
$scope.currentEntityInternal = null;
|
||||
$scope.currentEntity = null;
|
||||
|
||||
if ($scope.entitySelected) {
|
||||
$scope.entitySelected(null);
|
||||
}
|
||||
|
@ -3000,6 +3036,7 @@ quayApp.directive('entitySearch', function () {
|
|||
}
|
||||
|
||||
if ($scope.isPersistent) {
|
||||
$scope.currentEntityInternal = entity;
|
||||
$scope.currentEntity = entity;
|
||||
}
|
||||
|
||||
|
@ -3124,6 +3161,16 @@ quayApp.directive('entitySearch', function () {
|
|||
$scope.$watch('inputTitle', function(title) {
|
||||
input.setAttribute('placeholder', title);
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if ($scope.currentEntityInternal != entity) {
|
||||
if (entity) {
|
||||
$scope.setEntityInternal(entity, false);
|
||||
} else {
|
||||
$scope.clearEntityInternal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -3661,6 +3708,145 @@ quayApp.directive('dropdownSelectMenu', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('setupTriggerDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger',
|
||||
'counter': '=counter',
|
||||
'canceled': '&canceled',
|
||||
'activated': '&activated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService) {
|
||||
$scope.show = function() {
|
||||
$scope.pullEntity = null;
|
||||
$scope.publicPull = true;
|
||||
$scope.showPullRequirements = false;
|
||||
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.setPublicPull = function(value) {
|
||||
$scope.publicPull = value;
|
||||
};
|
||||
|
||||
$scope.checkAnalyze = function(isValid) {
|
||||
if (!isValid) {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
$scope.showPullRequirements = false;
|
||||
$scope.checkingPullRequirements = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = true;
|
||||
$scope.showPullRequirements = true;
|
||||
$scope.pullRequirements = null;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger.config
|
||||
};
|
||||
|
||||
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
|
||||
if (resp['status'] == 'publicbase') {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
} else if (resp['namespace']) {
|
||||
$scope.publicPull = false;
|
||||
|
||||
if (resp['robots'] && resp['robots'].length > 0) {
|
||||
$scope.pullEntity = resp['robots'][0];
|
||||
} else {
|
||||
$scope.pullEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = false;
|
||||
}, function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
$scope.checkingPullRequirements = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.activate = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger['config']
|
||||
};
|
||||
|
||||
if ($scope.pullEntity) {
|
||||
data['pull_robot'] = $scope.pullEntity['name'];
|
||||
}
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
if ($scope.counter && $scope.trigger && $scope.repository) {
|
||||
$scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('trigger', check);
|
||||
$scope.$watch('counter', check);
|
||||
$scope.$watch('repository', check);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
|
||||
quayApp.directive('triggerSetupGithub', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -3670,15 +3856,18 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger'
|
||||
'trigger': '=trigger',
|
||||
'analyze': '&analyze'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.analyzeCounter = 0;
|
||||
$scope.setupReady = false;
|
||||
$scope.loading = true;
|
||||
|
||||
$scope.handleLocationInput = function(location) {
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
};
|
||||
|
||||
$scope.handleLocationSelected = function(datum) {
|
||||
|
@ -3689,6 +3878,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.currentLocation = location;
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': true});
|
||||
};
|
||||
|
||||
$scope.selectRepo = function(repo, org) {
|
||||
|
@ -3727,6 +3917,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3739,12 +3930,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
} else {
|
||||
$scope.currentLocation = null;
|
||||
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
|
||||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -3789,7 +3982,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
});
|
||||
};
|
||||
|
||||
loadSources();
|
||||
var check = function() {
|
||||
if ($scope.repository && $scope.trigger) {
|
||||
loadSources();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('repository', check);
|
||||
$scope.$watch('trigger', check);
|
||||
|
||||
$scope.$watch('currentRepo', function(repo) {
|
||||
$scope.selectRepoInternal(repo);
|
||||
|
@ -4409,6 +4609,17 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
});
|
||||
};
|
||||
|
||||
$rootScope.$watch('description', function(description) {
|
||||
if (!description) {
|
||||
description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||
}
|
||||
|
||||
// Note: We set the content of the description tag manually here rather than using Angular binding
|
||||
// because we need the <meta> tag to have a default description that is not of the form "{{ description }}",
|
||||
// we read by tools that do not properly invoke the Angular code.
|
||||
$('#descriptionTag').attr('content', description);
|
||||
});
|
||||
|
||||
$rootScope.$on('$routeUpdate', function(){
|
||||
if ($location.search()['tab']) {
|
||||
changeTab($location.search()['tab']);
|
||||
|
@ -4425,7 +4636,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
if (current.$$route.description) {
|
||||
$rootScope.description = current.$$route.description;
|
||||
} else {
|
||||
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||
$rootScope.description = '';
|
||||
}
|
||||
|
||||
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
||||
|
|
|
@ -1178,6 +1178,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
|
||||
$scope.showTriggerSetupCounter = 0;
|
||||
|
||||
$scope.getBadgeFormat = function(format, repo) {
|
||||
if (!repo) { return; }
|
||||
|
||||
|
@ -1467,65 +1469,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$scope.setupTrigger = function(trigger) {
|
||||
$scope.triggerSetupReady = false;
|
||||
$scope.currentSetupTrigger = trigger;
|
||||
|
||||
trigger['_pullEntity'] = null;
|
||||
trigger['_publicPull'] = true;
|
||||
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
$scope.showTriggerSetupCounter++;
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
$scope.cancelSetupTrigger = function(trigger) {
|
||||
if ($scope.currentSetupTrigger != trigger) { return; }
|
||||
|
||||
$scope.finishSetupTrigger = function(trigger) {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.currentSetupTrigger = null;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': trigger['config']
|
||||
};
|
||||
|
||||
if (trigger['_pullEntity']) {
|
||||
data['pull_robot'] = trigger['_pullEntity']['name'];
|
||||
}
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
trigger['pull_robot'] = resp['pull_robot'];
|
||||
}, function(resp) {
|
||||
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
if (!$scope.currentSetupTrigger) { return; }
|
||||
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.deleteTrigger($scope.currentSetupTrigger);
|
||||
$scope.currentSetupTrigger = null;
|
||||
$scope.deleteTrigger(trigger);
|
||||
};
|
||||
|
||||
$scope.startTrigger = function(trigger) {
|
||||
|
@ -1620,7 +1572,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
}
|
||||
|
||||
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
|
||||
$routeParams, $http, Features) {
|
||||
$routeParams, $http, UIService, Features) {
|
||||
$scope.Features = Features;
|
||||
|
||||
if ($routeParams['migrate']) {
|
||||
|
@ -1657,8 +1609,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.githubClientId = KeyService.githubClientId;
|
||||
$scope.authorizedApps = null;
|
||||
|
||||
$('.form-change').popover();
|
||||
|
||||
$scope.logsShown = 0;
|
||||
$scope.invoicesShown = 0;
|
||||
|
||||
|
@ -1748,7 +1698,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
};
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
$('#changeEmailForm').popover('hide');
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
|
||||
$scope.updatingUser = true;
|
||||
$scope.changeEmailSent = false;
|
||||
|
||||
|
@ -1763,16 +1714,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
$scope.changeEmailForm.$setPristine();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
|
||||
$scope.changeEmailError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changeEmailForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.changePassword = function() {
|
||||
$('#changePasswordForm').popover('hide');
|
||||
UIService.hidePopover('#changePasswordForm');
|
||||
|
||||
$scope.updatingUser = true;
|
||||
$scope.changePasswordSuccess = false;
|
||||
|
||||
|
@ -1790,11 +1738,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
UserService.load();
|
||||
}, function(result) {
|
||||
$scope.updatingUser = false;
|
||||
|
||||
$scope.changePasswordError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changePasswordForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changePasswordForm', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -2185,7 +2129,7 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
|||
loadOrganization();
|
||||
}
|
||||
|
||||
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features) {
|
||||
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features, UIService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
// Load the list of plans.
|
||||
|
@ -2226,10 +2170,12 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
|||
};
|
||||
|
||||
$scope.$watch('organizationEmail', function(e) {
|
||||
$('#changeEmailForm').popover('hide');
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
});
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
UIService.hidePopover('#changeEmailForm');
|
||||
|
||||
$scope.changingOrganization = true;
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
|
@ -2245,10 +2191,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
|||
$scope.organization = org;
|
||||
}, function(result) {
|
||||
$scope.changingOrganization = false;
|
||||
$scope.changeEmailError = result.data.message;
|
||||
$timeout(function() {
|
||||
$('#changeEmailForm').popover('show');
|
||||
});
|
||||
UIService.showFormError('#changeEmailForm', result);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -222,7 +222,17 @@ ImageHistoryTree.prototype.draw = function(container) {
|
|||
if (d.image.command && d.image.command.length) {
|
||||
html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>';
|
||||
}
|
||||
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
||||
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
||||
|
||||
var tags = d.tags || [];
|
||||
html += '<span class="tooltip-tags tags">';
|
||||
for (var i = 0; i < tags.length; ++i) {
|
||||
var tag = tags[i];
|
||||
var kind = 'default';
|
||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
|
||||
}
|
||||
html += '</span>';
|
||||
|
||||
return html;
|
||||
})
|
||||
|
||||
|
@ -330,6 +340,23 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Expands the given collapsed node in the tree.
|
||||
*/
|
||||
ImageHistoryTree.prototype.expandCollapsed_ = function(imageNode) {
|
||||
var index = imageNode.parent.children.indexOf(imageNode);
|
||||
if (index < 0 || imageNode.encountered.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: we start at 1 since the 0th encountered node is the parent.
|
||||
imageNode.parent.children.splice(index, 1, imageNode.encountered[1]);
|
||||
this.maxHeight_ = this.determineMaximumHeight_(this.root_);
|
||||
this.update_(this.root_);
|
||||
this.updateDimensions_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Builds the root node for the tree.
|
||||
*/
|
||||
|
@ -632,7 +659,10 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
||||
.text(function(d) { return d.name; })
|
||||
.on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
|
||||
.on("click", function(d) {
|
||||
if (d.image) { that.changeImage_(d.image.id); }
|
||||
if (d.collapsed) { that.expandCollapsed_(d); }
|
||||
})
|
||||
.on('mouseover', tip.show)
|
||||
.on('mouseout', tip.hide);
|
||||
|
||||
|
@ -685,9 +715,9 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
if (d.virtual) {
|
||||
return 'virtual';
|
||||
}
|
||||
if (!currentImage) {
|
||||
return '';
|
||||
}
|
||||
if (!currentImage) {
|
||||
return '';
|
||||
}
|
||||
return d.image.id == currentImage.id ? 'current' : '';
|
||||
});
|
||||
|
||||
|
@ -709,7 +739,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
|||
if (tag == currentTag) {
|
||||
kind = 'success';
|
||||
}
|
||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"">' + tag + '</span>';
|
||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '">' + tag + '</span>';
|
||||
}
|
||||
return html;
|
||||
});
|
||||
|
@ -1686,7 +1716,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate)
|
|||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
nv.utils.windoweResize(chart.update);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
||||
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<dd am-time-ago="parseDate(image.value.created)"></dd>
|
||||
<dt>Compressed Image Size</dt>
|
||||
<dd><span class="context-tooltip"
|
||||
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
|
||||
</dd>
|
||||
|
||||
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
||||
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
||||
<span title="{{change.file}}">
|
||||
<span data-title="{{change.file}}">
|
||||
<span style="color: #888;">
|
||||
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
||||
</span>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<div quay-include="{'Features.BILLING': 'landing-normal.html', '!Features.BILLING': 'landing-login.html'}" onload="chromify()">
|
||||
<span class="quay-spinner"></span>
|
||||
</div>
|
||||
</div>
|
|
@ -106,7 +106,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Client Secret: <i class="fa fa-lock fa-lg" title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
|
||||
<td>Client Secret: <i class="fa fa-lock fa-lg" data-title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
|
||||
<td>
|
||||
{{ application.client_secret }}
|
||||
</td>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<div class="section">
|
||||
<div class="repo-option">
|
||||
<input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1">
|
||||
<i class="fa fa-unlock fa-large" title="Public Repository"></i>
|
||||
<i class="fa fa-unlock fa-large" data-title="Public Repository"></i>
|
||||
|
||||
<div class="option-description">
|
||||
<label for="publicrepo"><strong>Public</strong></label>
|
||||
|
@ -60,7 +60,7 @@
|
|||
</div>
|
||||
<div class="repo-option">
|
||||
<input type="radio" id="privaterepo" name="publicorprivate" ng-model="repo.is_public" value="0">
|
||||
<i class="fa fa-lock fa-large" title="Private Repository"></i>
|
||||
<i class="fa fa-lock fa-large" data-title="Private Repository"></i>
|
||||
|
||||
<div class="option-description">
|
||||
<label for="privaterepo"><strong>Private</strong></label>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<span ng-if="isUserNamespace">under your personal namespace</span>
|
||||
<span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to
|
||||
<b style="border-bottom: 1px dotted black;" data-html="true"
|
||||
title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
||||
data-title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
||||
{{ planRequired.title }}
|
||||
</b>.
|
||||
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" title="Member Usage Logs" bs-tooltip="tooltip.title">
|
||||
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.name }}" data-title="Member Usage Logs" bs-tooltip="tooltip.title">
|
||||
<i class="fa fa-book"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="row hidden-xs">
|
||||
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
|
||||
Team Permissions
|
||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
|
||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
|
||||
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar-right">
|
||||
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-plus"></i>
|
||||
Create New Organization
|
||||
</button>
|
||||
</a>
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fa fa-caret-square-o-right"></i>
|
||||
Convert account
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-list.png" data-title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">A central collection of repositories</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -57,7 +57,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
||||
<div class="col-md-5 col-md-pull-7">
|
||||
<div class="tour-section-title">Organization settings at a glance</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -73,7 +73,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-logs.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-logs.png" data-title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">Logging for comprehensive analysis</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -94,7 +94,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" data-title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||
<div class="col-md-5 col-md-pull-7">
|
||||
<div class="tour-section-title">Teams simplify access controls</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -115,7 +115,7 @@
|
|||
</div>
|
||||
|
||||
<div class="tour-section row">
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
||||
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" data-title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
||||
<div class="col-md-5">
|
||||
<div class="tour-section-title">Fine-grained control of sharing</div>
|
||||
<div class="tour-section-description">
|
||||
|
@ -133,13 +133,13 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar-right button-bar-bottom">
|
||||
<a href="/organizations/new/" title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<a href="/organizations/new/" data-title="Starts the process to create a new organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-plus"></i>
|
||||
Create New Organization
|
||||
</button>
|
||||
</a>
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<a href="/user/?migrate" ng-show="!user.anonymous" data-title="Starts the process to convert this account into an organization" bs-tooltip="tooltip.title">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fa fa-caret-square-o-right"></i>
|
||||
Convert account
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have unlimited public repositories">
|
||||
data-title="All plans have unlimited public repositories">
|
||||
<span class="hidden-sm-inline">Public Repositories</span>
|
||||
<span class="visible-sm-inline">Public Repos</span>
|
||||
</span>
|
||||
|
@ -15,49 +15,49 @@
|
|||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="SSL encryption is enabled end-to-end for all operations">
|
||||
data-title="SSL encryption is enabled end-to-end for all operations">
|
||||
SSL Encryption
|
||||
</span>
|
||||
<i class="fa fa-lock visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||
data-title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||
Robot accounts
|
||||
</span>
|
||||
<i class="fa fa-wrench visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Repository images can be built directly from Dockerfiles">
|
||||
data-title="Repository images can be built directly from Dockerfiles">
|
||||
Dockerfile Build
|
||||
</span>
|
||||
<i class="fa fa-upload visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
Teams
|
||||
</span>
|
||||
<i class="fa fa-group visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||
data-title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||
Logging
|
||||
</span>
|
||||
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Administrators can view and download the full invoice history for their organization">
|
||||
data-title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have a 14-day free trial">
|
||||
data-title="All plans have a 14-day free trial">
|
||||
<span class="hidden-sm-inline">14-Day Free Trial</span>
|
||||
<span class="visible-sm-inline">14-Day Trial</span>
|
||||
</span>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
<!-- Status Image -->
|
||||
<a ng-href="/repository/{{ repo.namespace }}/{{ repo.name }}" ng-if="repo && repo.name">
|
||||
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" title="Docker Repository on Quay.io">
|
||||
<img ng-src="/repository/{{ repo.namespace }}/{{ repo.name }}/status?token={{ repo.status_token }}" data-title="Docker Repository on Quay.io">
|
||||
</a>
|
||||
|
||||
<!-- Embed formats -->
|
||||
|
@ -271,7 +271,7 @@
|
|||
</div>
|
||||
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div>
|
||||
<div class="trigger-pull-credentials" ng-if="trigger.is_active && trigger.pull_robot">
|
||||
<span class="context-tooltip" title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
<span class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</span>
|
||||
<span class="entity-reference" entity="trigger.pull_robot"></span>
|
||||
|
@ -279,7 +279,7 @@
|
|||
</td>
|
||||
<td style="white-space: nowrap;">
|
||||
<div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build History" bs-tooltip="tooltip.title" data-container="body"
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Build History" bs-tooltip="tooltip.title" data-container="body"
|
||||
ng-click="loadTriggerBuildHistory(trigger)">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<b class="caret"></b>
|
||||
|
@ -297,7 +297,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dropdown" style="display: inline-block">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" data-title="Trigger Settings" bs-tooltip="tooltip.title" data-container="body">
|
||||
<i class="fa fa-cog"></i>
|
||||
<b class="caret"></b>
|
||||
</button>
|
||||
|
@ -377,76 +377,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Auth dialog -->
|
||||
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
||||
shown="!!shownToken" counter="shownTokenCounter">
|
||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<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">Setup new build trigger</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="trigger-option-section">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="width: 114px">
|
||||
<div class="context-tooltip" title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="!isNamespaceAdmin(repo.namespace)" style="color: #aaa;">
|
||||
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repo.namespace }}</strong>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repo.namespace)">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="currentSetupTrigger._publicPull ? 'active btn-info' : ''" ng-click="currentSetupTrigger._publicPull = true">Public</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="currentSetupTrigger._publicPull ? '' : 'active btn-info'" ng-click="currentSetupTrigger._publicPull = false">
|
||||
<i class="fa fa-wrench"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="!currentSetupTrigger._publicPull">
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div class="entity-search" namespace="repo.namespace" include-teams="false"
|
||||
input-title="'Select robot account for pulling...'"
|
||||
is-organization="repo.is_organization"
|
||||
is-persistent="true"
|
||||
current-entity="currentSetupTrigger._pullEntity"
|
||||
filter="['robot']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="currentSetupTrigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!currentSetupTrigger.$ready || (!currentSetupTrigger._publicPull && !currentSetupTrigger._pullEntity)"
|
||||
ng-click="finishSetupTrigger(currentSetupTrigger)">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Setup trigger dialog-->
|
||||
<div class="setup-trigger-dialog" repository="repo"
|
||||
trigger="currentSetupTrigger"
|
||||
canceled="cancelSetupTrigger(trigger)"
|
||||
counter="showTriggerSetupCounter"></div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotchangeModal">
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<i class="fa fa-archive"></i>
|
||||
<a href="/repository/{{ repo.namespace }}/{{ repo.name }}/build/{{ currentBuild.id }}/buildpack"
|
||||
style="display: inline-block; margin-left: 6px" bs-tooltip="tooltip.title"
|
||||
title="View the uploaded build package for this build">Build Package</a>
|
||||
data-title="View the uploaded build package for this build">Build Package</a>
|
||||
</span>
|
||||
</div>
|
||||
<span class="phase-icon" ng-class="build.phase"></span>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="button-bar-right">
|
||||
<a href="/new/">
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-upload user-tool" title="Create new repository"></i>
|
||||
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
|
||||
Create Repository
|
||||
</button>
|
||||
</a>
|
||||
|
@ -60,11 +60,11 @@
|
|||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||
</div>
|
||||
<div class="page-controls">
|
||||
<button class="btn btn-default" title="Previous Page" bs-tooltip="title" ng-show="page > 1"
|
||||
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
|
||||
ng-click="movePublicPage(-1)">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-default" title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
|
||||
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
|
||||
ng-click="movePublicPage(1)">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
|
|
|
@ -69,10 +69,10 @@
|
|||
<td>
|
||||
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
|
||||
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
|
||||
title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||
{{ authInfo.application.name }}
|
||||
</a>
|
||||
<span ng-if="!authInfo.application.url" title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||
<span ng-if="!authInfo.application.url" data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||
{{ authInfo.application.name }}
|
||||
</span>
|
||||
<span class="by">{{ authInfo.application.organization.name }}</span>
|
||||
|
@ -80,7 +80,7 @@
|
|||
<td>
|
||||
<span class="label label-default scope"
|
||||
ng-class="{'repo:admin': 'label-primary', 'repo:write': 'label-success', 'repo:create': 'label-success'}[scopeInfo.scope]"
|
||||
ng-repeat="scopeInfo in authInfo.scopes" title="{{ scopeInfo.description }}" bs-tooltip>
|
||||
ng-repeat="scopeInfo in authInfo.scopes" data-title="{{ scopeInfo.description }}" bs-tooltip>
|
||||
{{ scopeInfo.scope }}
|
||||
</span>
|
||||
</td>
|
||||
|
@ -124,8 +124,8 @@
|
|||
<div class="panel-title">Change e-mail address</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class="form-change col-md-6" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
|
||||
data-content="{{ changeEmailError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
||||
<form class="form-change col-md-6" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()"
|
||||
ng-show="!awaitingConfirmation && !registering">
|
||||
<input type="email" class="form-control" placeholder="Your new e-mail address" ng-model="cuser.email" required>
|
||||
<button class="btn btn-primary" ng-disabled="changeEmailForm.$invalid || cuser.email == user.email" type="submit">Change E-mail Address</button>
|
||||
</form>
|
||||
|
@ -146,11 +146,12 @@
|
|||
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
||||
|
||||
<div ng-show="!updatingUser" class="panel-body">
|
||||
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
|
||||
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
||||
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
|
||||
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()"
|
||||
ng-show="!awaitingConfirmation && !registering">
|
||||
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
|
||||
match="cuser.password" required>
|
||||
match="cuser.password" required ng-pattern="/^.{8,}$/">
|
||||
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
|
||||
analytics-on analytics-event="change_pass">Change Password</button>
|
||||
</form>
|
||||
|
@ -169,7 +170,7 @@
|
|||
<div class="panel-title">GitHub Login:</div>
|
||||
<div class="panel-body">
|
||||
<div ng-show="githubLogin" class="lead col-md-8">
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px;" title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||
<b><a href="https://github.com/{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
|
||||
</div>
|
||||
<div ng-show="!githubLogin" class="col-md-8">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<!-- Builds -->
|
||||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||
bs-tooltip=""
|
||||
title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
|
||||
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
|
||||
ng-show="repo.can_write || buildHistory.length">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-tasks fa-lg"></i>
|
||||
|
@ -51,15 +51,15 @@
|
|||
<!-- 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" data-placement="top">
|
||||
<button class="btn btn-default" data-title="Repository Settings" bs-tooltip="tooltip" 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="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<span id="copyClipboard" class="input-group-addon" data-title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -145,10 +145,10 @@
|
|||
</ul>
|
||||
</div>
|
||||
<span class="right-tag-controls">
|
||||
<i class="fa fa-tag" title="Tags" bs-tooltip="title">
|
||||
<i class="fa fa-tag" data-title="Tags" bs-tooltip="title">
|
||||
<span class="tag-count">{{getTagCount(repo)}}</span>
|
||||
</i>
|
||||
<i class="fa fa-archive" title="Images" bs-tooltip="title">
|
||||
<i class="fa fa-archive" data-title="Images" bs-tooltip="title">
|
||||
<span class="tag-count">{{imageHistory.value.length}}</span>
|
||||
</i>
|
||||
</span>
|
||||
|
@ -162,7 +162,7 @@
|
|||
<dd am-time-ago="parseDate(currentTag.image.created)"></dd>
|
||||
<dt>Total Compressed Size</dt>
|
||||
<dd><span class="context-tooltip"
|
||||
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
bs-tooltip="tooltip.title" data-container="body">{{ getTotalSize(currentTag) | bytes }}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
|
@ -171,7 +171,7 @@
|
|||
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
|
||||
<span class="size-limiter">
|
||||
<span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}"
|
||||
bs-tooltip="" title="{{ image.size | bytes }}"></span>
|
||||
bs-tooltip="" data-title="{{ image.size | bytes }}"></span>
|
||||
</span>
|
||||
<span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
|
||||
</div>
|
||||
|
@ -199,14 +199,14 @@
|
|||
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
|
||||
<dt>Compressed Image Size</dt>
|
||||
<dd><span class="context-tooltip"
|
||||
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
data-title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||
bs-tooltip="tooltip.title" data-container="body">{{ currentImage.size | bytes }}</span>
|
||||
</dd>
|
||||
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
|
||||
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
|
||||
<pre class="formatted-command trimmed"
|
||||
data-html="true"
|
||||
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
|
||||
bs-tooltip="" data-title="{{ getTooltipCommand(currentImage) }}"
|
||||
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
|
||||
</dd>
|
||||
</dl>
|
||||
|
@ -216,17 +216,17 @@
|
|||
<div class="changes-container small-changes-container"
|
||||
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
||||
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
|
||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" data-title="Files Added"
|
||||
bs-tooltip="tooltip.title" data-placement="top">
|
||||
<i class="fa fa-plus-square"></i>
|
||||
<b>{{currentImageChanges.added.length}}</b>
|
||||
</span>
|
||||
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
|
||||
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" data-title="Files Removed"
|
||||
bs-tooltip="tooltip.title" data-placement="top">
|
||||
<i class="fa fa-minus-square"></i>
|
||||
<b>{{currentImageChanges.removed.length}}</b>
|
||||
</span>
|
||||
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
|
||||
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" data-title="Files Changed"
|
||||
bs-tooltip="tooltip.title" data-placement="top">
|
||||
<i class="fa fa-pencil-square"></i>
|
||||
<b>{{currentImageChanges.changed.length}}</b>
|
||||
|
@ -237,15 +237,15 @@
|
|||
<div class="well well-sm">
|
||||
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
||||
<i class="fa fa-plus-square"></i>
|
||||
<span title="{{file}}">{{file}}</span>
|
||||
<span data-title="{{file}}">{{file}}</span>
|
||||
</div>
|
||||
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
||||
<i class="fa fa-minus-square"></i>
|
||||
<span title="{{file}}">{{file}}</span>
|
||||
<span data-title="{{file}}">{{file}}</span>
|
||||
</div>
|
||||
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
||||
<i class="fa fa-pencil-square"></i>
|
||||
<span title="{{file}}">{{file}}</span>
|
||||
<span data-title="{{file}}">{{file}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
||||
|
@ -296,7 +296,7 @@
|
|||
<!--<i class="fa fa-archive"></i>-->
|
||||
<span class="image-listing-circle"></span>
|
||||
<span class="image-listing-line"></span>
|
||||
<span class="context-tooltip image-listing-id" bs-tooltip="" title="{{ getFirstTextLine(image.comment) }}"
|
||||
<span class="context-tooltip image-listing-id" bs-tooltip="" data-title="{{ getFirstTextLine(image.comment) }}"
|
||||
data-html="true">
|
||||
{{ image.id.substr(0, 12) }}
|
||||
</span>
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<!-- ,typeahead.js@0.10.1 -->
|
||||
|
||||
<script src="/static/lib/loading-bar.js"></script>
|
||||
<script src="/static/lib/angular-strap.min.js"></script>
|
||||
<script src="/static/lib/angular-strap.js"></script>
|
||||
<script src="/static/lib/angular-strap.tpl.min.js"></script>
|
||||
|
||||
{% if mixpanel_key %}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% block added_meta %}
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="{% raw %}{{ description }}{% endraw %}"></meta>
|
||||
<meta id="descriptionTag" name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories."></meta>
|
||||
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
||||
<meta name="fragment" content="!" />
|
||||
{% endblock %}
|
||||
|
|
Binary file not shown.
|
@ -17,7 +17,7 @@ from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, Reposi
|
|||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList)
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.webhook import Webhook, WebhookList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||
|
@ -87,6 +87,9 @@ class ApiTestCase(unittest.TestCase):
|
|||
|
||||
rv = client.open(final_url, **open_kwargs)
|
||||
msg = '%s %s: %s expected: %s' % (method, final_url, rv.status_code, expected_status)
|
||||
if rv.status_code != expected_status:
|
||||
print rv.data
|
||||
|
||||
self.assertEqual(rv.status_code, expected_status, msg)
|
||||
|
||||
def setUp(self):
|
||||
|
@ -1198,6 +1201,130 @@ class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
|||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
class TestActivateBuildTrigger0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ActivateBuildTrigger, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ActivateBuildTrigger, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byePublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="public/publicrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 403, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
class TestBuildTriggerAnalyze0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestRepositoryImageChangesPtsgPublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -19,7 +19,7 @@ from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, Repo
|
|||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList)
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.webhook import Webhook, WebhookList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||
UserAuthorizationList, UserAuthorization)
|
||||
|
@ -292,6 +292,24 @@ class TestCreateNewUser(ApiTestCase):
|
|||
expected_code=400)
|
||||
|
||||
self.assertEquals('The username already exists', json['message'])
|
||||
|
||||
def test_trycreatetooshort(self):
|
||||
json = self.postJsonResponse(User,
|
||||
data=dict(username='a',
|
||||
password='password',
|
||||
email='test@example.com'),
|
||||
expected_code=400)
|
||||
|
||||
self.assertEquals('Invalid username a: Username must be between 4 and 30 characters in length', json['error_description'])
|
||||
|
||||
def test_trycreateregexmismatch(self):
|
||||
json = self.postJsonResponse(User,
|
||||
data=dict(username='auserName',
|
||||
password='password',
|
||||
email='test@example.com'),
|
||||
expected_code=400)
|
||||
|
||||
self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+', json['error_description'])
|
||||
|
||||
def test_createuser(self):
|
||||
data = self.postResponse(User,
|
||||
|
@ -1605,6 +1623,15 @@ class FakeBuildTrigger(BuildTriggerBase):
|
|||
def manual_start(self, auth_token, config):
|
||||
return ('foo', ['bar'], 'build-name', 'subdir')
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
return 'http://some/url'
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
if not 'dockerfile' in config:
|
||||
return None
|
||||
|
||||
return config['dockerfile']
|
||||
|
||||
|
||||
class TestBuildTriggers(ApiTestCase):
|
||||
def test_list_build_triggers(self):
|
||||
|
@ -1653,6 +1680,82 @@ class TestBuildTriggers(ApiTestCase):
|
|||
self.assertEquals(0, len(json['triggers']))
|
||||
|
||||
|
||||
def test_analyze_fake_trigger(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
database.BuildTriggerService.create(name='fakeservice')
|
||||
|
||||
# Add a new fake trigger.
|
||||
repo = model.get_repository(ADMIN_ACCESS_USER, 'simple')
|
||||
user = model.get_user(ADMIN_ACCESS_USER)
|
||||
trigger = model.create_build_trigger(repo, 'fakeservice', 'sometoken', user)
|
||||
|
||||
# Analyze the trigger's dockerfile: First, no dockerfile.
|
||||
trigger_config = {}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Could not read the Dockerfile for the trigger', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Second, missing FROM in dockerfile.
|
||||
trigger_config = {'dockerfile': 'MAINTAINER me'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('warning', analyze_json['status'])
|
||||
self.assertEquals('No FROM line found in the Dockerfile', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Third, dockerfile with public repo.
|
||||
trigger_config = {'dockerfile': 'FROM somerepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('publicbase', analyze_json['status'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Fourth, dockerfile with private repo with an invalid path.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/somepath'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('warning', analyze_json['status'])
|
||||
self.assertEquals('"localhost:5000/somepath" is not a valid Quay repository path', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Fifth, dockerfile with private repo that does not exist.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/nothere/randomrepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Repository "localhost:5000/nothere/randomrepo" was not found', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Sixth, dockerfile with private repo that the user cannot see.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/randomuser/randomrepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Repository "localhost:5000/randomuser/randomrepo" was not found', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Seventh, dockerfile with private repo that the user see.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/devtable/complex'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('analyzed', analyze_json['status'])
|
||||
self.assertEquals('devtable', analyze_json['namespace'])
|
||||
self.assertEquals('complex', analyze_json['name'])
|
||||
self.assertEquals(False, analyze_json['is_public'])
|
||||
self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', analyze_json['robots'][0]['name'])
|
||||
|
||||
|
||||
def test_fake_trigger(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
|
|
36
tools/renderinvoice.py
Normal file
36
tools/renderinvoice.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from app import stripe
|
||||
from app import app
|
||||
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
|
||||
from data import model
|
||||
|
||||
import argparse
|
||||
|
||||
from flask import Flask, current_app
|
||||
from flask_mail import Mail
|
||||
|
||||
def sendInvoice(invoice_id):
|
||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||
if not invoice['customer']:
|
||||
print 'No customer found'
|
||||
return
|
||||
|
||||
customer_id = invoice['customer']
|
||||
user = model.get_user_or_org_by_customer_id(customer_id)
|
||||
if not user:
|
||||
print 'No user found for customer %s' % (customer_id)
|
||||
return
|
||||
|
||||
|
||||
with app.app_context():
|
||||
file_data = renderInvoiceToPdf(invoice, user)
|
||||
with open('invoice.pdf', 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print 'Invoice output as invoice.pdf'
|
||||
|
||||
parser = argparse.ArgumentParser(description='Generate an invoice')
|
||||
parser.add_argument('invoice_id', help='The invoice ID')
|
||||
args = parser.parse_args()
|
||||
sendInvoice(args.invoice_id)
|
81
util/dockerfileparse.py
Normal file
81
util/dockerfileparse.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import re
|
||||
|
||||
LINE_CONTINUATION_REGEX = re.compile('\s*\\\s*\n')
|
||||
COMMAND_REGEX = re.compile('([A-Z]+)\s(.*)')
|
||||
|
||||
COMMENT_CHARACTER = '#'
|
||||
|
||||
class ParsedDockerfile(object):
|
||||
def __init__(self, commands):
|
||||
self.commands = commands
|
||||
|
||||
def get_commands_of_kind(self, kind):
|
||||
return [command for command in self.commands if command['command'] == kind]
|
||||
|
||||
def get_base_image(self):
|
||||
image_and_tag = self.get_base_image_and_tag()
|
||||
if not image_and_tag:
|
||||
return None
|
||||
|
||||
return self.base_image_from_repo_identifier(image_and_tag)
|
||||
|
||||
@staticmethod
|
||||
def base_image_from_repo_identifier(image_and_tag):
|
||||
# Note:
|
||||
# Dockerfile images references can be of multiple forms:
|
||||
# server:port/some/path
|
||||
# somepath
|
||||
# server/some/path
|
||||
# server/some/path:tag
|
||||
# server:port/some/path:tag
|
||||
parts = image_and_tag.strip().split(':')
|
||||
|
||||
if len(parts) == 1:
|
||||
# somepath
|
||||
return parts[0]
|
||||
|
||||
# Otherwise, determine if the last part is a port
|
||||
# or a tag.
|
||||
if parts[-1].find('/') >= 0:
|
||||
# Last part is part of the hostname.
|
||||
return image_and_tag
|
||||
|
||||
# Remaining cases:
|
||||
# server/some/path:tag
|
||||
# server:port/some/path:tag
|
||||
return ':'.join(parts[0:-1])
|
||||
|
||||
def get_base_image_and_tag(self):
|
||||
from_commands = self.get_commands_of_kind('FROM')
|
||||
if not from_commands:
|
||||
return None
|
||||
|
||||
return from_commands[-1]['parameters']
|
||||
|
||||
|
||||
def strip_comments(contents):
|
||||
lines = [line for line in contents.split('\n') if not line.startswith(COMMENT_CHARACTER)]
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def join_continued_lines(contents):
|
||||
return LINE_CONTINUATION_REGEX.sub('', contents)
|
||||
|
||||
|
||||
def parse_dockerfile(contents):
|
||||
contents = join_continued_lines(strip_comments(contents))
|
||||
lines = [line for line in contents.split('\n') if len(line) > 0]
|
||||
|
||||
commands = []
|
||||
for line in lines:
|
||||
match_command = COMMAND_REGEX.match(line)
|
||||
if match_command:
|
||||
command = match_command.group(1)
|
||||
parameters = match_command.group(2)
|
||||
|
||||
commands.append({
|
||||
'command': command,
|
||||
'parameters': parameters
|
||||
})
|
||||
|
||||
return ParsedDockerfile(commands)
|
|
@ -10,10 +10,16 @@ def validate_email(email_address):
|
|||
|
||||
|
||||
def validate_username(username):
|
||||
# Minimum length of 2, maximum length of 255, no url unsafe characters
|
||||
return (re.search(r'[^a-z0-9_]', username) is None and
|
||||
len(username) >= 4 and
|
||||
len(username) <= 30)
|
||||
# Based off the restrictions defined in the Docker Registry API spec
|
||||
regex_match = (re.search(r'[^a-z0-9_]', username) is None)
|
||||
if not regex_match:
|
||||
return (False, 'Username must match expression [a-z0-9_]+')
|
||||
|
||||
length_match = (len(username) >= 4 and len(username) <= 30)
|
||||
if not length_match:
|
||||
return (False, 'Username must be between 4 and 30 characters in length')
|
||||
|
||||
return (True, '')
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
|
|
|
@ -14,12 +14,14 @@ from zipfile import ZipFile
|
|||
from functools import partial
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
from uuid import uuid4
|
||||
|
||||
from data.queue import dockerfile_build_queue
|
||||
from data import model
|
||||
from workers.worker import Worker
|
||||
from app import app, userfiles as user_files
|
||||
from util.safetar import safe_extractall
|
||||
from util.dockerfileparse import parse_dockerfile, ParsedDockerfile
|
||||
|
||||
|
||||
root_logger = logging.getLogger('')
|
||||
|
@ -33,6 +35,7 @@ logger = logging.getLogger(__name__)
|
|||
build_logs = app.config['BUILDLOGS']
|
||||
|
||||
TIMEOUT_PERIOD_MINUTES = 20
|
||||
CACHE_EXPIRATION_PERIOD_HOURS = 24
|
||||
|
||||
|
||||
class StatusWrapper(object):
|
||||
|
@ -94,6 +97,9 @@ class StreamingDockerClient(Client):
|
|||
|
||||
|
||||
class DockerfileBuildContext(object):
|
||||
image_id_to_cache_time = {}
|
||||
private_repo_tags = set()
|
||||
|
||||
def __init__(self, build_context_dir, dockerfile_subdir, repo, tag_names,
|
||||
push_token, build_uuid, pull_credentials=None):
|
||||
self._build_dir = build_context_dir
|
||||
|
@ -104,6 +110,7 @@ class DockerfileBuildContext(object):
|
|||
self._status = StatusWrapper(build_uuid)
|
||||
self._build_logger = partial(build_logs.append_log_message, build_uuid)
|
||||
self._pull_credentials = pull_credentials
|
||||
self._public_repos = set()
|
||||
|
||||
# Note: We have two different clients here because we (potentially) login
|
||||
# with both, but with different credentials that we do not want shared between
|
||||
|
@ -113,29 +120,27 @@ class DockerfileBuildContext(object):
|
|||
|
||||
dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir,
|
||||
'Dockerfile')
|
||||
self._num_steps = DockerfileBuildContext.__count_steps(dockerfile_path)
|
||||
|
||||
# Compute the number of steps
|
||||
with open(dockerfile_path, 'r') as dockerfileobj:
|
||||
self._parsed_dockerfile = parse_dockerfile(dockerfileobj.read())
|
||||
self._num_steps = len(self._parsed_dockerfile.commands)
|
||||
|
||||
logger.debug('Will build and push to repo %s with tags named: %s' %
|
||||
(self._repo, self._tag_names))
|
||||
|
||||
def __enter__(self):
|
||||
self.__cleanup_containers()
|
||||
self.__evict_expired_images()
|
||||
self.__cleanup()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, value, traceback):
|
||||
self.__cleanup_containers()
|
||||
self.__cleanup()
|
||||
|
||||
shutil.rmtree(self._build_dir)
|
||||
|
||||
@staticmethod
|
||||
def __count_steps(dockerfile_path):
|
||||
with open(dockerfile_path, 'r') as dockerfileobj:
|
||||
steps = 0
|
||||
for line in dockerfileobj.readlines():
|
||||
stripped = line.strip()
|
||||
if stripped and stripped[0] is not '#':
|
||||
steps += 1
|
||||
return steps
|
||||
|
||||
@staticmethod
|
||||
def __total_completion(statuses, total_images):
|
||||
percentage_with_sizes = float(len(statuses.values()))/total_images
|
||||
|
@ -151,6 +156,11 @@ class DockerfileBuildContext(object):
|
|||
self._build_cl.login(self._pull_credentials['username'], self._pull_credentials['password'],
|
||||
registry=self._pull_credentials['registry'], reauth=True)
|
||||
|
||||
# Pull the image, in case it was updated since the last build
|
||||
base_image = self._parsed_dockerfile.get_base_image()
|
||||
self._build_logger('Pulling base image: %s' % base_image)
|
||||
self._build_cl.pull(base_image)
|
||||
|
||||
# Start the build itself.
|
||||
logger.debug('Starting build.')
|
||||
|
||||
|
@ -260,7 +270,39 @@ class DockerfileBuildContext(object):
|
|||
|
||||
raise RuntimeError(message)
|
||||
|
||||
def __cleanup(self):
|
||||
def __is_repo_public(self, repo_name):
|
||||
if repo_name in self._public_repos:
|
||||
return True
|
||||
|
||||
repo_portions = repo_name.split('/')
|
||||
registry_hostname = 'index.docker.io'
|
||||
local_repo_name = repo_name
|
||||
if len(repo_portions) > 2:
|
||||
registry_hostname = repo_portions[0]
|
||||
local_repo_name = '/'.join(repo_portions[1:])
|
||||
|
||||
repo_url_template = '%s://%s/v1/repositories/%s/images'
|
||||
protocols = ['https', 'http']
|
||||
secure_repo_url, repo_url = [repo_url_template % (protocol, registry_hostname, local_repo_name)
|
||||
for protocol in protocols]
|
||||
|
||||
try:
|
||||
|
||||
try:
|
||||
repo_info = requests.get(secure_repo_url)
|
||||
except requests.exceptions.SSLError:
|
||||
repo_info = requests.get(repo_url)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
if repo_info.status_code / 100 == 2:
|
||||
self._public_repos.add(repo_name)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def __cleanup_containers(self):
|
||||
# First clean up any containers that might be holding the images
|
||||
for running in self._build_cl.containers(quiet=True):
|
||||
logger.debug('Killing container: %s' % running['Id'])
|
||||
|
@ -271,40 +313,62 @@ class DockerfileBuildContext(object):
|
|||
logger.debug('Removing container: %s' % container['Id'])
|
||||
self._build_cl.remove_container(container['Id'])
|
||||
|
||||
# Iterate all of the images and remove the ones that the public registry
|
||||
# doesn't know about, this should preserve base images.
|
||||
images_to_remove = set()
|
||||
repos = set()
|
||||
def __evict_expired_images(self):
|
||||
logger.debug('Cleaning images older than %s hours.', CACHE_EXPIRATION_PERIOD_HOURS)
|
||||
now = datetime.now()
|
||||
verify_removed = set()
|
||||
|
||||
for image in self._build_cl.images():
|
||||
images_to_remove.add(image['Id'])
|
||||
image_id = image[u'Id']
|
||||
created = datetime.fromtimestamp(image[u'Created'])
|
||||
|
||||
for tag in image['RepoTags']:
|
||||
tag_repo = tag.split(':')[0]
|
||||
if tag_repo != '<none>':
|
||||
repos.add(tag_repo)
|
||||
# If we don't have a cache time, use the created time (e.g. worker reboot)
|
||||
cache_time = self.image_id_to_cache_time.get(image_id, created)
|
||||
expiration = cache_time + timedelta(hours=CACHE_EXPIRATION_PERIOD_HOURS)
|
||||
|
||||
for repo in repos:
|
||||
repo_url = 'https://index.docker.io/v1/repositories/%s/images' % repo
|
||||
repo_info = requests.get(repo_url)
|
||||
if repo_info.status_code / 100 == 2:
|
||||
for repo_image in repo_info.json():
|
||||
if repo_image['id'] in images_to_remove:
|
||||
logger.debug('Image was deemed public: %s' % repo_image['id'])
|
||||
images_to_remove.remove(repo_image['id'])
|
||||
if expiration < now:
|
||||
logger.debug('Removing expired image: %s' % image_id)
|
||||
|
||||
for to_remove in images_to_remove:
|
||||
logger.debug('Removing private image: %s' % to_remove)
|
||||
try:
|
||||
self._build_cl.remove_image(to_remove)
|
||||
except APIError:
|
||||
# Sometimes an upstream image removed this one
|
||||
pass
|
||||
for tag in image['RepoTags']:
|
||||
# We can forget about this particular tag if it was indeed one of our renamed tags
|
||||
self.private_repo_tags.discard(tag)
|
||||
|
||||
verify_removed.add(image_id)
|
||||
try:
|
||||
self._build_cl.remove_image(image_id)
|
||||
except APIError:
|
||||
# Sometimes an upstream image removed this one
|
||||
pass
|
||||
|
||||
# Verify that our images were actually removed
|
||||
for image in self._build_cl.images():
|
||||
if image['Id'] in images_to_remove:
|
||||
if image['Id'] in verify_removed:
|
||||
raise RuntimeError('Image was not removed: %s' % image['Id'])
|
||||
|
||||
def __cleanup(self):
|
||||
# Iterate all of the images and rename the ones that aren't public. This should preserve
|
||||
# base images and also allow the cache to function.
|
||||
now = datetime.now()
|
||||
for image in self._build_cl.images():
|
||||
image_id = image[u'Id']
|
||||
|
||||
if image_id not in self.image_id_to_cache_time:
|
||||
logger.debug('Setting image %s cache time to %s', image_id, now)
|
||||
self.image_id_to_cache_time[image_id] = now
|
||||
|
||||
for tag in image['RepoTags']:
|
||||
tag_repo = ParsedDockerfile.base_image_from_repo_identifier(tag)
|
||||
if tag_repo != '<none>':
|
||||
if tag_repo in self.private_repo_tags:
|
||||
logger.debug('Repo is private and has already been renamed: %s' % tag_repo)
|
||||
elif self.__is_repo_public(tag_repo):
|
||||
logger.debug('Repo was deemed public: %s', tag_repo)
|
||||
else:
|
||||
new_name = str(uuid4())
|
||||
logger.debug('Private repo tag being renamed %s -> %s', tag, new_name)
|
||||
self._build_cl.tag(image_id, new_name)
|
||||
self._build_cl.remove_image(tag)
|
||||
self.private_repo_tags.add(new_name)
|
||||
|
||||
class DockerfileBuildWorker(Worker):
|
||||
def __init__(self, *vargs, **kwargs):
|
||||
|
@ -317,6 +381,7 @@ class DockerfileBuildWorker(Worker):
|
|||
'application/octet-stream': DockerfileBuildWorker.__prepare_dockerfile,
|
||||
'application/x-tar': DockerfileBuildWorker.__prepare_tarball,
|
||||
'application/gzip': DockerfileBuildWorker.__prepare_tarball,
|
||||
'application/x-gzip': DockerfileBuildWorker.__prepare_tarball,
|
||||
}
|
||||
|
||||
self._timeout = Event()
|
||||
|
@ -327,7 +392,7 @@ class DockerfileBuildWorker(Worker):
|
|||
|
||||
# Save the zip file to temp somewhere
|
||||
with TemporaryFile() as zip_file:
|
||||
zip_file.write(request_file.raw)
|
||||
zip_file.write(request_file.content)
|
||||
to_extract = ZipFile(zip_file)
|
||||
to_extract.extractall(build_dir)
|
||||
|
||||
|
|
Reference in a new issue