Add support for pull credentials on builds and build triggers

This commit is contained in:
Joseph Schorr 2014-03-27 18:33:13 -04:00
parent 1fc3c922a9
commit 2006917e03
17 changed files with 355 additions and 37 deletions

View file

@ -176,6 +176,7 @@ class RepositoryBuildTrigger(BaseModel):
auth_token = CharField() auth_token = CharField()
config = TextField(default='{}') config = TextField(default='{}')
write_token = ForeignKeyField(AccessToken, null=True) write_token = ForeignKeyField(AccessToken, null=True)
pull_user = ForeignKeyField(User, null=True, related_name='pulluser')
class EmailConfirmation(BaseModel): class EmailConfirmation(BaseModel):

View file

@ -154,6 +154,16 @@ def create_robot(robot_shortname, parent):
raise DataModelException(ex.message) raise DataModelException(ex.message)
def lookup_robot(robot_username):
joined = User.select().join(FederatedLogin).join(LoginService)
found = list(joined.where(LoginService.name == 'quayrobot',
User.username == robot_username))
if not found or len(found) < 1 or not found[0].robot:
return None
return found[0]
def verify_robot(robot_username, password): def verify_robot(robot_username, password):
joined = User.select().join(FederatedLogin).join(LoginService) joined = User.select().join(FederatedLogin).join(LoginService)
found = list(joined.where(FederatedLogin.service_ident == password, found = list(joined.where(FederatedLogin.service_ident == password,
@ -1449,6 +1459,20 @@ def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
display_name=display_name, trigger=trigger, display_name=display_name, trigger=trigger,
resource_key=dockerfile_id) resource_key=dockerfile_id)
def get_pull_credentials(trigger):
if not trigger.pull_user:
return None
try:
login_info = FederatedLogin.get(user=trigger.pull_user)
except FederatedLogin.DoesNotExist:
return None
return {
'username': trigger.pull_user.username,
'password': login_info.service_ident,
'registry': 'quay.io' # TODO: Is there a better way to do this?
}
def create_webhook(repo, params_obj): def create_webhook(repo, params_obj):
return Webhook.create(repository=repo, parameters=json.dumps(params_obj)) return Webhook.create(repository=repo, parameters=json.dumps(params_obj))
@ -1506,11 +1530,12 @@ def log_action(kind_name, user_or_organization_name, performer=None,
metadata_json=json.dumps(metadata), datetime=timestamp) metadata_json=json.dumps(metadata), datetime=timestamp)
def create_build_trigger(repo, service_name, auth_token, user): def create_build_trigger(repo, service_name, auth_token, user, pull_user=None):
service = BuildTriggerService.get(name=service_name) service = BuildTriggerService.get(name=service_name)
trigger = RepositoryBuildTrigger.create(repository=repo, service=service, trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
auth_token=auth_token, auth_token=auth_token,
connected_user=user) connected_user=user,
pull_user=None)
return trigger return trigger

View file

@ -33,6 +33,13 @@ def get_job_config(build_obj):
def trigger_view(trigger): def trigger_view(trigger):
def user_view(user):
return {
'name': user.username,
'kind': 'user',
'is_robot': user.robot,
}
if trigger and trigger.uuid: if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger) config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
@ -41,7 +48,8 @@ def trigger_view(trigger):
'config': config_dict, 'config': config_dict,
'id': trigger.uuid, 'id': trigger.uuid,
'connected_user': trigger.connected_user.username, 'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict) 'is_active': build_trigger.is_active(config_dict),
'pull_user': user_view(trigger.pull_user) if trigger.pull_user else None
} }
return None return None
@ -88,6 +96,29 @@ class RepositoryBuildList(RepositoryParamResource):
'type': 'string', 'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found', 'description': 'Subdirectory in which the Dockerfile can be found',
}, },
'pull_credentials': {
'type': 'object',
'description': 'Credentials used by the builder when pulling images',
'required': [
'username',
'password',
'registry'
],
'properties': {
'username': {
'type': 'string',
'description': 'The username for the pull'
},
'password': {
'type': 'string',
'description': 'The password for the pull'
},
'registry': {
'type': 'string',
'description': 'The registry hostname for the pull'
},
}
}
}, },
}, },
} }
@ -116,6 +147,7 @@ class RepositoryBuildList(RepositoryParamResource):
dockerfile_id = request_json['file_id'] dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else '' subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
pull_credentials = request_json.get('pull_credentials', None)
# Check if the dockerfile resource has already been used. If so, then it # Check if the dockerfile resource has already been used. If so, then it
# can only be reused if the user has access to the repository for which it # can only be reused if the user has access to the repository for which it
@ -130,7 +162,8 @@ class RepositoryBuildList(RepositoryParamResource):
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
display_name = user_files.get_file_checksum(dockerfile_id) display_name = user_files.get_file_checksum(dockerfile_id)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True) build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True,
pull_credentials=pull_credentials)
resp = build_status_view(build_request, True) resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace, repository)

View file

@ -15,7 +15,8 @@ from endpoints.common import start_build
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
TriggerActivationException, EmptyRepositoryException) TriggerActivationException, EmptyRepositoryException)
from data import model from data import model
from auth.permissions import UserAdminPermission from auth.permissions import UserAdminPermission, AdministerOrganizationPermission
from util.names import parse_robot_username
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -132,9 +133,21 @@ class BuildTriggerActivate(RepositoryParamResource):
schemas = { schemas = {
'BuildTriggerActivateRequest': { 'BuildTriggerActivateRequest': {
'id': 'BuildTriggerActivateRequest', 'id': 'BuildTriggerActivateRequest',
'type': 'object',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object', 'type': 'object',
'description': 'Arbitrary json.', 'description': 'Arbitrary json.',
}, },
'pull_robot': {
'type': 'string',
'description': 'The name of the robot that will be used to pull images.'
}
}
},
} }
@require_repo_admin @require_repo_admin
@ -154,7 +167,27 @@ class BuildTriggerActivate(RepositoryParamResource):
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can(): if user_permission.can():
new_config_dict = request.get_json() # Update the pull robot (if any).
pull_robot_name = request.get_json().get('pull_robot', None)
if pull_robot_name:
pull_robot = model.lookup_robot(pull_robot_name)
if not pull_robot:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, shortname) = parse_robot_username(pull_robot_name)
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
# Make sure the namespace matches that of the trigger.
if robot_namespace != namespace:
raise Unauthorized()
# Set the pull robot.
trigger.pull_user = pull_robot
# Update the config.
new_config_dict = request.get_json()['config']
token_name = 'Build Trigger: %s' % trigger.service.name token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name, token = model.create_delegate_token(namespace, repository, token_name,
@ -185,6 +218,7 @@ class BuildTriggerActivate(RepositoryParamResource):
log_action('setup_repo_trigger', namespace, log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace, {'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name, 'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_user': trigger.pull_user.username if trigger.pull_user else None,
'config': final_config}, repo=repo) 'config': final_config}, repo=repo)
return trigger_view(trigger) return trigger_view(trigger)
@ -214,8 +248,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
dockerfile_id, tags, name, subdir = specs dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
pull_credentials = model.get_pull_credentials(trigger)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True) build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
pull_credentials=pull_credentials)
resp = build_status_view(build_request, True) resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace, repository)

View file

@ -100,7 +100,7 @@ def check_repository_usage(user_or_org, plan_found):
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
trigger=None): trigger=None, pull_credentials=None):
host = urlparse.urlparse(request.url).netloc host = urlparse.urlparse(request.url).netloc
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name) repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
@ -112,7 +112,9 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
'docker_tags': tags, 'docker_tags': tags,
'repository': repo_path, 'repository': repo_path,
'build_subdir': subdir, 'build_subdir': subdir,
'pull_credentials': pull_credentials,
} }
build_request = model.create_repository_build(repository, token, job_config, build_request = model.create_repository_build(repository, token, job_config,
dockerfile_id, build_name, dockerfile_id, build_name,
trigger) trigger)

View file

@ -73,8 +73,10 @@ def build_trigger_webhook(namespace, repository, trigger_uuid):
# This was just a validation request, we don't need to build anything # This was just a validation request, we don't need to build anything
return make_response('Okay') return make_response('Okay')
pull_credentials = model.get_pull_credentials(trigger)
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger) start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
pull_credentials=pull_credentials)
return make_response('Okay') return make_response('Okay')

View file

@ -257,7 +257,7 @@ def populate_database():
new_user_1.stripe_id = TEST_STRIPE_ID new_user_1.stripe_id = TEST_STRIPE_ID
new_user_1.save() new_user_1.save()
model.create_robot('dtrobot', new_user_1) dtrobot = model.create_robot('dtrobot', new_user_1)
new_user_2 = model.create_user('public', 'password', new_user_2 = model.create_user('public', 'password',
'jacob.moshenko@gmail.com') 'jacob.moshenko@gmail.com')
@ -268,6 +268,8 @@ def populate_database():
new_user_3.verified = True new_user_3.verified = True
new_user_3.save() new_user_3.save()
model.create_robot('anotherrobot', new_user_3)
new_user_4 = model.create_user('randomuser', 'password', 'no4@thanks.com') new_user_4 = model.create_user('randomuser', 'password', 'no4@thanks.com')
new_user_4.verified = True new_user_4.verified = True
new_user_4.save() new_user_4.save()
@ -330,7 +332,7 @@ def populate_database():
token = model.create_access_token(building, 'write') token = model.create_access_token(building, 'write')
trigger = model.create_build_trigger(building, 'github', '123authtoken', trigger = model.create_build_trigger(building, 'github', '123authtoken',
new_user_1) new_user_1, pull_user = dtrobot)
trigger.config = json.dumps({ trigger.config = json.dumps({
'build_source': 'jakedt/testconnect', 'build_source': 'jakedt/testconnect',
'subdir': '', 'subdir': '',
@ -354,6 +356,8 @@ def populate_database():
org.stripe_id = TEST_STRIPE_ID org.stripe_id = TEST_STRIPE_ID
org.save() org.save()
model.create_robot('coolrobot', org)
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html', oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
client_id='deadbeef') client_id='deadbeef')

View file

@ -3590,3 +3590,37 @@ pre.command:before {
cursor: pointer; cursor: pointer;
margin-right: 4px; margin-right: 4px;
} }
.trigger-pull-credentials {
margin-top: 4px;
padding-left: 26px;
font-size: 12px;
}
.trigger-pull-credentials .context-tooltip {
color: gray;
margin-right: 4px;
}
.trigger-description .trigger-description-subtitle {
display: inline-block;
margin-right: 34px;
}
.trigger-option-section:not(:last-child) {
border-bottom: 1px solid #eee;
padding-bottom: 16px;
margin-bottom: 16px;
}
.trigger-option-section .entity-search-element .twitter-typeahead {
width: 370px;
}
.trigger-option-section .entity-search-element input {
width: 100%;
}
.trigger-option-section table td {
padding: 6px;
}

View file

@ -10,7 +10,7 @@
</span> </span>
</div> </div>
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short"> <div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!trigger.config.subdir && !short">
<span>Dockerfile: <span><span class="trigger-description-subtitle">Dockerfile:</span>
<a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/Dockerfile" target="_blank"> <a href="https://github.com/{{ trigger.config.build_source }}/tree/{{ trigger.config.master_branch || 'master' }}/Dockerfile" target="_blank">
//Dockerfile //Dockerfile
</a> </a>

View file

@ -2504,7 +2504,8 @@ quayApp.directive('entitySearch', function () {
'isOrganization': '=isOrganization', 'isOrganization': '=isOrganization',
'isPersistent': '=isPersistent', 'isPersistent': '=isPersistent',
'currentEntity': '=currentEntity', 'currentEntity': '=currentEntity',
'clearNow': '=clearNow' 'clearNow': '=clearNow',
'filter': '=filter',
}, },
controller: function($scope, $element, Restangular, UserService, ApiService) { controller: function($scope, $element, Restangular, UserService, ApiService) {
$scope.lazyLoading = true; $scope.lazyLoading = true;
@ -2628,6 +2629,19 @@ quayApp.directive('entitySearch', function () {
var datums = []; var datums = [];
for (var i = 0; i < data.results.length; ++i) { for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i]; var entity = data.results[i];
if ($scope.filter) {
var allowed = $scope.filter;
var found = 'user';
if (entity.kind == 'user') {
found = entity.is_robot ? 'robot' : 'user';
} else if (entity.kind == 'team') {
found = 'team';
}
if (allowed.indexOf(found)) {
continue;
}
}
datums.push({ datums.push({
'value': entity.name, 'value': entity.name,
'tokens': [entity.name], 'tokens': [entity.name],

View file

@ -1158,7 +1158,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
fetchRepository(); fetchRepository();
} }
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location) { function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService) {
var namespace = $routeParams.namespace; var namespace = $routeParams.namespace;
var name = $routeParams.name; var name = $routeParams.name;
@ -1462,6 +1462,10 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
$scope.setupTrigger = function(trigger) { $scope.setupTrigger = function(trigger) {
$scope.triggerSetupReady = false; $scope.triggerSetupReady = false;
$scope.currentSetupTrigger = trigger; $scope.currentSetupTrigger = trigger;
trigger['_pullEntity'] = null;
trigger['_publicPull'] = true;
$('#setupTriggerModal').modal({}); $('#setupTriggerModal').modal({});
$('#setupTriggerModal').on('hidden.bs.modal', function () { $('#setupTriggerModal').on('hidden.bs.modal', function () {
$scope.$apply(function() { $scope.$apply(function() {
@ -1470,6 +1474,10 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
}); });
}; };
$scope.isNamespaceAdmin = function(namespace) {
return UserService.isNamespaceAdmin(namespace);
};
$scope.finishSetupTrigger = function(trigger) { $scope.finishSetupTrigger = function(trigger) {
$('#setupTriggerModal').modal('hide'); $('#setupTriggerModal').modal('hide');
$scope.currentSetupTrigger = null; $scope.currentSetupTrigger = null;
@ -1479,7 +1487,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
'trigger_uuid': trigger.id 'trigger_uuid': trigger.id
}; };
ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) { 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['is_active'] = true;
}, function(resp) { }, function(resp) {
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1); $scope.triggers.splice($scope.triggers.indexOf(trigger), 1);

View file

@ -270,6 +270,12 @@
Setting up trigger Setting up trigger
</div> </div>
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></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_user">
<span class="context-tooltip" title="The credentials used by the builder when pulling images" bs-tooltip>
Pull Credentials:
</span>
<span class="entity-reference" entity="trigger.pull_user"></span>
</div>
</td> </td>
<td style="white-space: nowrap;"> <td style="white-space: nowrap;">
<div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active"> <div class="dropdown" style="display: inline-block" ng-visible="trigger.is_active">
@ -387,7 +393,45 @@
<h4 class="modal-title">Setup new build trigger</h4> <h4 class="modal-title">Setup new build trigger</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="trigger-description-element" ng-switch on="currentSetupTrigger.service"> <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 ng-switch-when="github">
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div> <div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
</div> </div>

Binary file not shown.

View file

@ -962,7 +962,7 @@ class TestBuildTriggerActivateSwo1DevtableShared(ApiTestCase):
self._run_test('POST', 403, 'reader', {}) self._run_test('POST', 403, 'reader', {})
def test_post_devtable(self): def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', {}) self._run_test('POST', 404, 'devtable', {'config': {}})
class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase): class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
@ -980,7 +980,7 @@ class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
self._run_test('POST', 403, 'reader', {}) self._run_test('POST', 403, 'reader', {})
def test_post_devtable(self): def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', {}) self._run_test('POST', 404, 'devtable', {'config': {}})
class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase): class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase):

View file

@ -982,6 +982,33 @@ class TestRequestRepoBuild(ApiTestCase):
assert len(json['builds']) > 0 assert len(json['builds']) > 0
def test_requestrepobuild_with_credentials(self):
self.login(ADMIN_ACCESS_USER)
# Ensure where not yet building.
json = self.getJsonResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
assert len(json['builds']) == 0
# Request a (fake) build.
pull_creds = {
'username': 'foo',
'password': 'bar',
'registry': 'baz'
}
self.postResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
data=dict(file_id='foobarbaz', pull_credentials=pull_creds),
expected_code=201)
# Check for the build.
json = self.getJsonResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/building'))
assert len(json['builds']) > 0
class TestWebhooks(ApiTestCase): class TestWebhooks(ApiTestCase):
def test_webhooks(self): def test_webhooks(self):
@ -1642,7 +1669,7 @@ class TestBuildTriggers(ApiTestCase):
trigger_config = {} trigger_config = {}
activate_json = self.postJsonResponse(BuildTriggerActivate, activate_json = self.postJsonResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid), params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data=trigger_config) data={'config': trigger_config})
self.assertEquals(True, activate_json['is_active']) self.assertEquals(True, activate_json['is_active'])
@ -1654,7 +1681,7 @@ class TestBuildTriggers(ApiTestCase):
# Make sure we cannot activate again. # Make sure we cannot activate again.
self.postResponse(BuildTriggerActivate, self.postResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid), params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data=trigger_config, data={'config': trigger_config},
expected_code=400) expected_code=400)
# Start a manual build. # Start a manual build.
@ -1667,6 +1694,69 @@ class TestBuildTriggers(ApiTestCase):
self.assertEquals(['bar'], start_json['job_config']['docker_tags']) self.assertEquals(['bar'], start_json['job_config']['docker_tags'])
def test_invalid_robot_account(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)
# Try to activate it with an invalid robot account.
trigger_config = {}
activate_json = self.postJsonResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data={'config': trigger_config, 'pull_robot': 'someinvalidrobot'},
expected_code=404)
def test_unauthorized_robot_account(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)
# Try to activate it with a robot account in the wrong namespace.
trigger_config = {}
activate_json = self.postJsonResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data={'config': trigger_config, 'pull_robot': 'freshuser+anotherrobot'},
expected_code=403)
def test_robot_account(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)
# Try to activate it with a robot account.
trigger_config = {}
activate_json = self.postJsonResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data={'config': trigger_config, 'pull_robot': ADMIN_ACCESS_USER + '+dtrobot'})
# Verify that the robot was saved.
self.assertEquals(True, activate_json['is_active'])
self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', activate_json['pull_user']['name'])
# Start a manual build.
start_json = self.postJsonResponse(ActivateBuildTrigger,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
expected_code=201)
assert 'id' in start_json
self.assertEquals("build-name", start_json['display_name'])
self.assertEquals(['bar'], start_json['job_config']['docker_tags'])
class TestUserAuthorizations(ApiTestCase): class TestUserAuthorizations(ApiTestCase):
def test_list_get_delete_user_authorizations(self): def test_list_get_delete_user_authorizations(self):

View file

@ -24,3 +24,6 @@ def parse_repository_name(f):
def format_robot_username(parent_username, robot_shortname): def format_robot_username(parent_username, robot_shortname):
return '%s+%s' % (parent_username, robot_shortname) return '%s+%s' % (parent_username, robot_shortname)
def parse_robot_username(robot_username):
return robot_username.split('+', 2)

View file

@ -56,15 +56,21 @@ def unwrap_stream(json_stream):
class DockerfileBuildContext(object): class DockerfileBuildContext(object):
def __init__(self, build_context_dir, dockerfile_subdir, repo, tag_names, def __init__(self, build_context_dir, dockerfile_subdir, repo, tag_names,
push_token, build_uuid): push_token, build_uuid, pull_credentials=None):
self._build_dir = build_context_dir self._build_dir = build_context_dir
self._dockerfile_subdir = dockerfile_subdir self._dockerfile_subdir = dockerfile_subdir
self._repo = repo self._repo = repo
self._tag_names = tag_names self._tag_names = tag_names
self._push_token = push_token self._push_token = push_token
self._cl = Client(timeout=1200)
self._status = StatusWrapper(build_uuid) self._status = StatusWrapper(build_uuid)
self._build_logger = partial(build_logs.append_log_message, build_uuid) self._build_logger = partial(build_logs.append_log_message, build_uuid)
self._pull_credentials = pull_credentials
# Note: We have two different clients here because we (potentially) login
# with both, but with different credentials that we do not want shared between
# the build and push operations.
self._push_cl = Client(timeout=1200)
self._build_cl = Client(timeout=1200)
dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir, dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir,
"Dockerfile") "Dockerfile")
@ -99,6 +105,13 @@ class DockerfileBuildContext(object):
return float(sent_bytes)/total_bytes*percentage_with_sizes return float(sent_bytes)/total_bytes*percentage_with_sizes
def build(self): def build(self):
# Login with the specified credentials (if any).
if self._pull_credentials:
logger.debug('Logging in with pull credentials.')
self.build_cl_.login(self._pull_credentials['username'], self._pull_credentials['password'],
registry=self._pull_credentials['registry'], reauth=True)
# Start the build itself.
logger.debug('Starting build.') logger.debug('Starting build.')
with self._status as status: with self._status as status:
@ -110,7 +123,7 @@ class DockerfileBuildContext(object):
logger.debug('Final context path: %s exists: %s' % logger.debug('Final context path: %s exists: %s' %
(context_path, os.path.exists(context_path))) (context_path, os.path.exists(context_path)))
build_status = self._cl.build(path=context_path, stream=True) build_status = self._build_cl.build(path=context_path, stream=True)
current_step = 0 current_step = 0
built_image = None built_image = None
@ -158,7 +171,7 @@ class DockerfileBuildContext(object):
logger.debug('Attempting login to registry: %s' % registry_endpoint) logger.debug('Attempting login to registry: %s' % registry_endpoint)
try: try:
self._cl.login('$token', self._push_token, registry=registry_endpoint) self._push_cl.login('$token', self._push_token, registry=registry_endpoint)
break break
except APIError: except APIError:
pass # Probably the wrong protocol pass # Probably the wrong protocol
@ -166,15 +179,15 @@ class DockerfileBuildContext(object):
for tag in self._tag_names: for tag in self._tag_names:
logger.debug('Tagging image %s as %s:%s' % logger.debug('Tagging image %s as %s:%s' %
(built_image, self._repo, tag)) (built_image, self._repo, tag))
self._cl.tag(built_image, self._repo, tag) self._push_cl.tag(built_image, self._repo, tag)
history = json.loads(self._cl.history(built_image)) history = json.loads(self._push_cl.history(built_image))
num_images = len(history) num_images = len(history)
with self._status as status: with self._status as status:
status['total_images'] = num_images status['total_images'] = num_images
logger.debug('Pushing to repo %s' % self._repo) logger.debug('Pushing to repo %s' % self._repo)
resp = self._cl.push(self._repo, stream=True) resp = self._push_cl.push(self._repo, stream=True)
for status_str in resp: for status_str in resp:
status = json.loads(status_str) status = json.loads(status_str)
@ -204,20 +217,20 @@ class DockerfileBuildContext(object):
def __cleanup(self): def __cleanup(self):
# First clean up any containers that might be holding the images # First clean up any containers that might be holding the images
for running in self._cl.containers(quiet=True): for running in self._build_cl.containers(quiet=True):
logger.debug('Killing container: %s' % running['Id']) logger.debug('Killing container: %s' % running['Id'])
self._cl.kill(running['Id']) self._build_cl.kill(running['Id'])
# Next, remove all of the containers (which should all now be killed) # Next, remove all of the containers (which should all now be killed)
for container in self._cl.containers(all=True, quiet=True): for container in self._build_cl.containers(all=True, quiet=True):
logger.debug('Removing container: %s' % container['Id']) logger.debug('Removing container: %s' % container['Id'])
self._cl.remove_container(container['Id']) self._build_cl.remove_container(container['Id'])
# Iterate all of the images and remove the ones that the public registry # Iterate all of the images and remove the ones that the public registry
# doesn't know about, this should preserve base images. # doesn't know about, this should preserve base images.
images_to_remove = set() images_to_remove = set()
repos = set() repos = set()
for image in self._cl.images(): for image in self._build_cl.images():
images_to_remove.add(image['Id']) images_to_remove.add(image['Id'])
for tag in image['RepoTags']: for tag in image['RepoTags']:
@ -237,13 +250,13 @@ class DockerfileBuildContext(object):
for to_remove in images_to_remove: for to_remove in images_to_remove:
logger.debug('Removing private image: %s' % to_remove) logger.debug('Removing private image: %s' % to_remove)
try: try:
self._cl.remove_image(to_remove) self._build_cl.remove_image(to_remove)
except APIError: except APIError:
# Sometimes an upstream image removed this one # Sometimes an upstream image removed this one
pass pass
# Verify that our images were actually removed # Verify that our images were actually removed
for image in self._cl.images(): for image in self._build_cl.images():
if image['Id'] in images_to_remove: if image['Id'] in images_to_remove:
raise RuntimeError('Image was not removed: %s' % image['Id']) raise RuntimeError('Image was not removed: %s' % image['Id'])
@ -291,6 +304,7 @@ class DockerfileBuildWorker(Worker):
tag_names = job_config['docker_tags'] tag_names = job_config['docker_tags']
build_subdir = job_config['build_subdir'] build_subdir = job_config['build_subdir']
repo = job_config['repository'] repo = job_config['repository']
pull_credentials = job_config.get('pull_credentials', None)
access_token = repository_build.access_token.code access_token = repository_build.access_token.code
@ -325,7 +339,7 @@ class DockerfileBuildWorker(Worker):
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names,
access_token, access_token,
repository_build.uuid) as build_ctxt: repository_build.uuid, pull_credentials) as build_ctxt:
try: try:
built_image = build_ctxt.build() built_image = build_ctxt.build()