Merge remote-tracking branch 'origin/master' into benjamins

This commit is contained in:
jakedt 2014-04-14 16:17:11 -04:00
commit 4946dca804
13 changed files with 759 additions and 131 deletions

View file

@ -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__)
@ -232,6 +233,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['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')
class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """

View file

@ -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
@ -46,6 +47,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
@ -168,7 +182,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()
@ -219,6 +232,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

View file

@ -295,7 +295,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'),

View file

@ -3463,7 +3463,7 @@ pre.command:before {
position: relative;
height: 100px;
height: 75px;
opacity: 1;
}
@ -3614,10 +3614,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 {

View file

@ -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()">

View 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">&times;</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>

View file

@ -2681,6 +2681,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; }
@ -2763,7 +2764,9 @@ quayApp.directive('entitySearch', function () {
};
$scope.clearEntityInternal = function() {
$scope.currentEntityInternal = null;
$scope.currentEntity = null;
if ($scope.entitySelected) {
$scope.entitySelected(null);
}
@ -2777,6 +2780,7 @@ quayApp.directive('entitySearch', function () {
}
if ($scope.isPersistent) {
$scope.currentEntityInternal = entity;
$scope.currentEntity = entity;
}
@ -2901,6 +2905,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;
@ -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 () {
var directiveDefinitionObject = {
priority: 0,
@ -3447,15 +3600,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) {
@ -3466,6 +3622,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) {
@ -3504,6 +3661,7 @@ quayApp.directive('triggerSetupGithub', function () {
$scope.locations = null;
$scope.trigger.$ready = false;
$scope.isInvalidLocation = false;
$scope.analyze({'isValid': false});
return;
}
@ -3516,12 +3674,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});
});
}
};
@ -3566,7 +3726,14 @@ quayApp.directive('triggerSetupGithub', function () {
});
};
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);

View file

@ -1165,6 +1165,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; }
@ -1454,65 +1456,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) {

View file

@ -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">&times;</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 -->
<!-- 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">

Binary file not shown.

View file

@ -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):

View file

@ -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)
@ -1623,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):
@ -1671,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)

72
util/dockerfileparse.py Normal file
View 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)