Merge remote-tracking branch 'origin/master' into benjamins
This commit is contained in:
commit
4946dca804
13 changed files with 759 additions and 131 deletions
|
@ -16,8 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
|
||||||
TriggerActivationException, EmptyRepositoryException,
|
TriggerActivationException, EmptyRepositoryException,
|
||||||
RepositoryReadException)
|
RepositoryReadException)
|
||||||
from data import model
|
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.names import parse_robot_username
|
||||||
|
from util.dockerfileparse import parse_dockerfile
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -232,6 +233,141 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
raise Unauthorized()
|
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['URL_HOST'])
|
||||||
|
|
||||||
|
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')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||||
class ActivateBuildTrigger(RepositoryParamResource):
|
class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
""" Custom verb to manually activate a build trigger. """
|
""" Custom verb to manually activate a build trigger. """
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
import io
|
import io
|
||||||
import os.path
|
import os.path
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import base64
|
||||||
|
|
||||||
from github import Github, UnknownObjectException, GithubException
|
from github import Github, UnknownObjectException, GithubException
|
||||||
from tempfile import SpooledTemporaryFile
|
from tempfile import SpooledTemporaryFile
|
||||||
|
@ -46,6 +47,19 @@ class BuildTrigger(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
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):
|
def list_build_sources(self, auth_token):
|
||||||
"""
|
"""
|
||||||
Take the auth information for the specific trigger type and load the
|
Take the auth information for the specific trigger type and load the
|
||||||
|
@ -168,7 +182,6 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def list_build_sources(self, auth_token):
|
def list_build_sources(self, auth_token):
|
||||||
gh_client = self._get_client(auth_token)
|
gh_client = self._get_client(auth_token)
|
||||||
usr = gh_client.get_user()
|
usr = gh_client.get_user()
|
||||||
|
@ -219,6 +232,41 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
|
|
||||||
raise RepositoryReadException(message)
|
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
|
@staticmethod
|
||||||
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
||||||
# Prepare the download and upload URLs
|
# Prepare the download and upload URLs
|
||||||
|
|
|
@ -295,7 +295,7 @@ def populate_database():
|
||||||
|
|
||||||
__generate_repository(new_user_1, 'complex',
|
__generate_repository(new_user_1, 'complex',
|
||||||
'Complex repository with many branches and tags.',
|
'Complex repository with many branches and tags.',
|
||||||
False, [(new_user_2, 'read')],
|
False, [(new_user_2, 'read'), (dtrobot[0], 'read')],
|
||||||
(2, [(3, [], 'v2.0'),
|
(2, [(3, [], 'v2.0'),
|
||||||
(1, [(1, [(1, [], ['prod'])],
|
(1, [(1, [(1, [], ['prod'])],
|
||||||
'staging'),
|
'staging'),
|
||||||
|
|
|
@ -3463,7 +3463,7 @@ pre.command:before {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
height: 100px;
|
height: 75px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3614,10 +3614,10 @@ pre.command:before {
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trigger-option-section:not(:last-child) {
|
.trigger-option-section:not(:first-child) {
|
||||||
border-bottom: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
padding-bottom: 16px;
|
padding-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trigger-option-section .entity-search-element .twitter-typeahead {
|
.trigger-option-section .entity-search-element .twitter-typeahead {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
|
<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">
|
<div class="dropdown">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
||||||
ng-click="lazyLoad()">
|
ng-click="lazyLoad()">
|
||||||
|
|
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>
|
169
static/js/app.js
169
static/js/app.js
|
@ -2681,6 +2681,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
||||||
$scope.lazyLoading = true;
|
$scope.lazyLoading = true;
|
||||||
$scope.isAdmin = false;
|
$scope.isAdmin = false;
|
||||||
|
$scope.currentEntityInternal = $scope.currentEntity;
|
||||||
|
|
||||||
$scope.lazyLoad = function() {
|
$scope.lazyLoad = function() {
|
||||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||||
|
@ -2763,7 +2764,9 @@ quayApp.directive('entitySearch', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.clearEntityInternal = function() {
|
$scope.clearEntityInternal = function() {
|
||||||
|
$scope.currentEntityInternal = null;
|
||||||
$scope.currentEntity = null;
|
$scope.currentEntity = null;
|
||||||
|
|
||||||
if ($scope.entitySelected) {
|
if ($scope.entitySelected) {
|
||||||
$scope.entitySelected(null);
|
$scope.entitySelected(null);
|
||||||
}
|
}
|
||||||
|
@ -2777,6 +2780,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.isPersistent) {
|
if ($scope.isPersistent) {
|
||||||
|
$scope.currentEntityInternal = entity;
|
||||||
$scope.currentEntity = entity;
|
$scope.currentEntity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2901,6 +2905,16 @@ quayApp.directive('entitySearch', function () {
|
||||||
$scope.$watch('inputTitle', function(title) {
|
$scope.$watch('inputTitle', function(title) {
|
||||||
input.setAttribute('placeholder', 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;
|
return directiveDefinitionObject;
|
||||||
|
@ -3438,6 +3452,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 () {
|
quayApp.directive('triggerSetupGithub', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
@ -3447,15 +3600,18 @@ quayApp.directive('triggerSetupGithub', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'trigger': '=trigger'
|
'trigger': '=trigger',
|
||||||
|
'analyze': '&analyze'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService) {
|
controller: function($scope, $element, ApiService) {
|
||||||
|
$scope.analyzeCounter = 0;
|
||||||
$scope.setupReady = false;
|
$scope.setupReady = false;
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
|
|
||||||
$scope.handleLocationInput = function(location) {
|
$scope.handleLocationInput = function(location) {
|
||||||
$scope.trigger['config']['subdir'] = location || '';
|
$scope.trigger['config']['subdir'] = location || '';
|
||||||
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||||||
|
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.handleLocationSelected = function(datum) {
|
$scope.handleLocationSelected = function(datum) {
|
||||||
|
@ -3466,6 +3622,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
||||||
$scope.currentLocation = location;
|
$scope.currentLocation = location;
|
||||||
$scope.trigger['config']['subdir'] = location || '';
|
$scope.trigger['config']['subdir'] = location || '';
|
||||||
$scope.isInvalidLocation = false;
|
$scope.isInvalidLocation = false;
|
||||||
|
$scope.analyze({'isValid': true});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.selectRepo = function(repo, org) {
|
$scope.selectRepo = function(repo, org) {
|
||||||
|
@ -3504,6 +3661,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
||||||
$scope.locations = null;
|
$scope.locations = null;
|
||||||
$scope.trigger.$ready = false;
|
$scope.trigger.$ready = false;
|
||||||
$scope.isInvalidLocation = false;
|
$scope.isInvalidLocation = false;
|
||||||
|
$scope.analyze({'isValid': false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3516,12 +3674,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
||||||
} else {
|
} else {
|
||||||
$scope.currentLocation = null;
|
$scope.currentLocation = null;
|
||||||
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||||||
|
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||||
}
|
}
|
||||||
}, function(resp) {
|
}, function(resp) {
|
||||||
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
|
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
|
||||||
$scope.locations = null;
|
$scope.locations = null;
|
||||||
$scope.trigger.$ready = false;
|
$scope.trigger.$ready = false;
|
||||||
$scope.isInvalidLocation = false;
|
$scope.isInvalidLocation = false;
|
||||||
|
$scope.analyze({'isValid': false});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -3566,7 +3726,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var check = function() {
|
||||||
|
if ($scope.repository && $scope.trigger) {
|
||||||
loadSources();
|
loadSources();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('repository', check);
|
||||||
|
$scope.$watch('trigger', check);
|
||||||
|
|
||||||
$scope.$watch('currentRepo', function(repo) {
|
$scope.$watch('currentRepo', function(repo) {
|
||||||
$scope.selectRepoInternal(repo);
|
$scope.selectRepoInternal(repo);
|
||||||
|
|
|
@ -1165,6 +1165,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||||
$scope.githubClientId = KeyService.githubClientId;
|
$scope.githubClientId = KeyService.githubClientId;
|
||||||
|
|
||||||
|
$scope.showTriggerSetupCounter = 0;
|
||||||
|
|
||||||
$scope.getBadgeFormat = function(format, repo) {
|
$scope.getBadgeFormat = function(format, repo) {
|
||||||
if (!repo) { return; }
|
if (!repo) { return; }
|
||||||
|
|
||||||
|
@ -1454,65 +1456,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setupTrigger = function(trigger) {
|
$scope.setupTrigger = function(trigger) {
|
||||||
$scope.triggerSetupReady = false;
|
|
||||||
$scope.currentSetupTrigger = trigger;
|
$scope.currentSetupTrigger = trigger;
|
||||||
|
$scope.showTriggerSetupCounter++;
|
||||||
trigger['_pullEntity'] = null;
|
|
||||||
trigger['_publicPull'] = true;
|
|
||||||
|
|
||||||
$('#setupTriggerModal').modal({});
|
|
||||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
|
||||||
$scope.$apply(function() {
|
|
||||||
$scope.cancelSetupTrigger();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isNamespaceAdmin = function(namespace) {
|
$scope.cancelSetupTrigger = function(trigger) {
|
||||||
return UserService.isNamespaceAdmin(namespace);
|
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.currentSetupTrigger = null;
|
||||||
|
$scope.deleteTrigger(trigger);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.startTrigger = function(trigger) {
|
$scope.startTrigger = function(trigger) {
|
||||||
|
|
|
@ -377,76 +377,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Auth dialog -->
|
<!-- Auth dialog -->
|
||||||
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
||||||
shown="!!shownToken" counter="shownTokenCounter">
|
shown="!!shownToken" counter="shownTokenCounter">
|
||||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Setup trigger dialog-->
|
||||||
<div class="modal fade" id="setupTriggerModal">
|
<div class="setup-trigger-dialog" repository="repo"
|
||||||
<div class="modal-dialog">
|
trigger="currentSetupTrigger"
|
||||||
<div class="modal-content">
|
canceled="cancelSetupTrigger(trigger)"
|
||||||
<div class="modal-header">
|
counter="showTriggerSetupCounter"></div>
|
||||||
<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" data-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 -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="cannotchangeModal">
|
<div class="modal fade" id="cannotchangeModal">
|
||||||
|
|
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.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.webhook import Webhook, WebhookList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||||
|
@ -87,6 +87,9 @@ class ApiTestCase(unittest.TestCase):
|
||||||
|
|
||||||
rv = client.open(final_url, **open_kwargs)
|
rv = client.open(final_url, **open_kwargs)
|
||||||
msg = '%s %s: %s expected: %s' % (method, final_url, rv.status_code, expected_status)
|
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)
|
self.assertEqual(rv.status_code, expected_status, msg)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1198,6 +1201,130 @@ class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', None)
|
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):
|
class TestRepositoryImageChangesPtsgPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
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.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.webhook import Webhook, WebhookList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||||
UserAuthorizationList, UserAuthorization)
|
UserAuthorizationList, UserAuthorization)
|
||||||
|
@ -1623,6 +1623,15 @@ class FakeBuildTrigger(BuildTriggerBase):
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config):
|
||||||
return ('foo', ['bar'], 'build-name', 'subdir')
|
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):
|
class TestBuildTriggers(ApiTestCase):
|
||||||
def test_list_build_triggers(self):
|
def test_list_build_triggers(self):
|
||||||
|
@ -1671,6 +1680,82 @@ class TestBuildTriggers(ApiTestCase):
|
||||||
self.assertEquals(0, len(json['triggers']))
|
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):
|
def test_fake_trigger(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
|
72
util/dockerfileparse.py
Normal file
72
util/dockerfileparse.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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[0]['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:
|
||||||
|
m = COMMAND_REGEX.match(line)
|
||||||
|
if m:
|
||||||
|
command = m.group(1)
|
||||||
|
parameters = m.group(2)
|
||||||
|
|
||||||
|
commands.append({
|
||||||
|
'command': command,
|
||||||
|
'parameters': parameters
|
||||||
|
})
|
||||||
|
|
||||||
|
return ParsedDockerfile(commands)
|
Reference in a new issue