diff --git a/data/database.py b/data/database.py
index d99f56c77..d6a67bd80 100644
--- a/data/database.py
+++ b/data/database.py
@@ -176,6 +176,7 @@ class RepositoryBuildTrigger(BaseModel):
auth_token = CharField()
config = TextField(default='{}')
write_token = ForeignKeyField(AccessToken, null=True)
+ pull_robot = ForeignKeyField(User, null=True, related_name='triggerpullrobot')
class EmailConfirmation(BaseModel):
@@ -244,6 +245,7 @@ class RepositoryBuild(BaseModel):
started = DateTimeField(default=datetime.now)
display_name = CharField()
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
+ pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot')
class QueueItem(BaseModel):
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 5a735014b..3cfa6c27f 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -154,6 +154,16 @@ def create_robot(robot_shortname, parent):
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):
joined = User.select().join(FederatedLogin).join(LoginService)
found = list(joined.where(FederatedLogin.service_ident == password,
@@ -1443,11 +1453,40 @@ def get_recent_repository_build(namespace_name, repository_name):
def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
- display_name, trigger=None):
+ display_name, trigger=None, pull_robot_name=None):
+ pull_robot = None
+ if pull_robot_name:
+ pull_robot = lookup_robot(pull_robot_name)
+
return RepositoryBuild.create(repository=repo, access_token=access_token,
job_config=json.dumps(job_config_obj),
display_name=display_name, trigger=trigger,
- resource_key=dockerfile_id)
+ resource_key=dockerfile_id,
+ pull_robot=pull_robot)
+
+
+def get_pull_robot_name(trigger):
+ if not trigger.pull_robot:
+ return None
+
+ return trigger.pull_robot.username
+
+
+def get_pull_credentials(robotname):
+ robot = lookup_robot(robotname)
+ if not robot:
+ return None
+
+ try:
+ login_info = FederatedLogin.get(user=robot)
+ except FederatedLogin.DoesNotExist:
+ return None
+
+ return {
+ 'username': robot.username,
+ 'password': login_info.service_ident,
+ 'registry': '%s://%s/v1/' % (app.config['URL_SCHEME'], app.config['URL_HOST']),
+ }
def create_webhook(repo, params_obj):
@@ -1506,11 +1545,12 @@ def log_action(kind_name, user_or_organization_name, performer=None,
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_robot=None):
service = BuildTriggerService.get(name=service_name)
trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
auth_token=auth_token,
- connected_user=user)
+ connected_user=user,
+ pull_robot=pull_robot)
return trigger
diff --git a/endpoints/api/build.py b/endpoints/api/build.py
index 63c85ad8e..9fa130054 100644
--- a/endpoints/api/build.py
+++ b/endpoints/api/build.py
@@ -10,8 +10,10 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic
from endpoints.common import start_build
from endpoints.trigger import BuildTrigger
from data import model
-from auth.permissions import ModifyRepositoryPermission
+from auth.auth_context import get_authenticated_user
+from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
from data.buildlogs import BuildStatusRetrievalError
+from util.names import parse_robot_username
logger = logging.getLogger(__name__)
@@ -33,7 +35,15 @@ def get_job_config(build_obj):
return None
+def user_view(user):
+ return {
+ 'name': user.username,
+ 'kind': 'user',
+ 'is_robot': user.robot,
+ }
+
def trigger_view(trigger):
+
if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
@@ -42,7 +52,8 @@ def trigger_view(trigger):
'config': config_dict,
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
- 'is_active': build_trigger.is_active(config_dict)
+ 'is_active': build_trigger.is_active(config_dict),
+ 'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
}
return None
@@ -67,6 +78,7 @@ def build_status_view(build_obj, can_write=False):
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
+ 'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
}
if can_write:
@@ -95,6 +107,10 @@ class RepositoryBuildList(RepositoryParamResource):
'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found',
},
+ 'pull_robot': {
+ 'type': 'string',
+ 'description': 'Username of a Quay robot account to use as pull credentials',
+ }
},
},
}
@@ -123,6 +139,22 @@ class RepositoryBuildList(RepositoryParamResource):
dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
+ pull_robot_name = request_json.get('pull_robot', None)
+
+ # Verify the security behind the pull robot.
+ if pull_robot_name:
+ result = parse_robot_username(pull_robot_name)
+ if result:
+ 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) = result
+ if not AdministerOrganizationPermission(robot_namespace).can():
+ raise Unauthorized()
+ else:
+ raise Unauthorized()
# 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
@@ -137,7 +169,8 @@ class RepositoryBuildList(RepositoryParamResource):
repo = model.get_repository(namespace, repository)
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_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)
diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py
index c28188057..c62367f52 100644
--- a/endpoints/api/trigger.py
+++ b/endpoints/api/trigger.py
@@ -16,7 +16,8 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
TriggerActivationException, EmptyRepositoryException,
RepositoryReadException)
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__)
@@ -139,7 +140,19 @@ class BuildTriggerActivate(RepositoryParamResource):
'BuildTriggerActivateRequest': {
'id': 'BuildTriggerActivateRequest',
'type': 'object',
- 'description': 'Arbitrary json.',
+ 'required': [
+ 'config'
+ ],
+ 'properties': {
+ 'config': {
+ 'type': 'object',
+ 'description': 'Arbitrary json.',
+ },
+ 'pull_robot': {
+ 'type': 'string',
+ 'description': 'The name of the robot that will be used to pull images.'
+ }
+ }
},
}
@@ -160,7 +173,27 @@ class BuildTriggerActivate(RepositoryParamResource):
user_permission = UserAdminPermission(trigger.connected_user.username)
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_robot = pull_robot
+
+ # Update the config.
+ new_config_dict = request.get_json()['config']
token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name,
@@ -191,6 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource):
log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name,
+ 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo)
return trigger_view(trigger)
@@ -220,8 +254,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository)
+ pull_robot_name = model.get_pull_robot_name(trigger)
- build_request = start_build(repo, dockerfile_id, tags, name, subdir, True)
+ build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
+ pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)
diff --git a/endpoints/common.py b/endpoints/common.py
index df233dfba..e25d7c797 100644
--- a/endpoints/common.py
+++ b/endpoints/common.py
@@ -107,7 +107,7 @@ def check_repository_usage(user_or_org, plan_found):
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
- trigger=None):
+ trigger=None, pull_robot_name=None):
host = urlparse.urlparse(request.url).netloc
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
@@ -118,16 +118,18 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
job_config = {
'docker_tags': tags,
'repository': repo_path,
- 'build_subdir': subdir,
+ 'build_subdir': subdir
}
+
build_request = model.create_repository_build(repository, token, job_config,
dockerfile_id, build_name,
- trigger)
+ trigger, pull_robot_name = pull_robot_name)
dockerfile_build_queue.put(json.dumps({
'build_uuid': build_request.uuid,
'namespace': repository.namespace,
'repository': repository.name,
+ 'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
}), retries_remaining=1)
metadata = {
diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py
index d92e7095e..93d5e413c 100644
--- a/endpoints/webhooks.py
+++ b/endpoints/webhooks.py
@@ -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
return make_response('Okay')
+ pull_robot_name = model.get_pull_robot_name(trigger)
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_robot_name=pull_robot_name)
return make_response('Okay')
diff --git a/initdb.py b/initdb.py
index fdb3f8e01..854f83f8a 100644
--- a/initdb.py
+++ b/initdb.py
@@ -257,7 +257,7 @@ def populate_database():
new_user_1.stripe_id = TEST_STRIPE_ID
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',
'jacob.moshenko@gmail.com')
@@ -268,6 +268,8 @@ def populate_database():
new_user_3.verified = True
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.verified = True
new_user_4.save()
@@ -330,7 +332,7 @@ def populate_database():
token = model.create_access_token(building, 'write')
trigger = model.create_build_trigger(building, 'github', '123authtoken',
- new_user_1)
+ new_user_1, pull_robot=dtrobot[0])
trigger.config = json.dumps({
'build_source': 'jakedt/testconnect',
'subdir': '',
@@ -366,6 +368,8 @@ def populate_database():
org.stripe_id = TEST_STRIPE_ID
org.save()
+ model.create_robot('coolrobot', org)
+
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
client_id='deadbeef')
diff --git a/requirements-nover.txt b/requirements-nover.txt
index bac4ba690..8bd0d7946 100644
--- a/requirements-nover.txt
+++ b/requirements-nover.txt
@@ -21,7 +21,7 @@ xhtml2pdf
logstash_formatter
redis
hiredis
-docker-py
+git+https://github.com/DevTable/docker-py.git
loremipsum
pygithub
flask-restful
diff --git a/static/css/quay.css b/static/css/quay.css
index ac7c01e66..919c0ddc9 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -3589,4 +3589,38 @@ pre.command:before {
.auth-info .scope {
cursor: pointer;
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;
}
\ No newline at end of file
diff --git a/static/directives/trigger-description.html b/static/directives/trigger-description.html
index 2a081aa69..1ce32ec32 100644
--- a/static/directives/trigger-description.html
+++ b/static/directives/trigger-description.html
@@ -10,7 +10,7 @@
-
Dockerfile:
+ Dockerfile:
//Dockerfile
diff --git a/static/js/app.js b/static/js/app.js
index 57a012a44..311b531fb 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -2644,7 +2644,8 @@ quayApp.directive('entitySearch', function () {
'isOrganization': '=isOrganization',
'isPersistent': '=isPersistent',
'currentEntity': '=currentEntity',
- 'clearNow': '=clearNow'
+ 'clearNow': '=clearNow',
+ 'filter': '=filter',
},
controller: function($scope, $element, Restangular, UserService, ApiService) {
$scope.lazyLoading = true;
@@ -2727,7 +2728,7 @@ quayApp.directive('entitySearch', function () {
entity['is_org_member'] = true;
}
- $scope.setEntityInternal(entity);
+ $scope.setEntityInternal(entity, false);
};
$scope.clearEntityInternal = function() {
@@ -2737,8 +2738,12 @@ quayApp.directive('entitySearch', function () {
}
};
- $scope.setEntityInternal = function(entity) {
- $(input).typeahead('val', $scope.isPersistent ? entity.name : '');
+ $scope.setEntityInternal = function(entity, updateTypeahead) {
+ if (updateTypeahead) {
+ $(input).typeahead('val', $scope.isPersistent ? entity.name : '');
+ } else {
+ $(input).val($scope.isPersistent ? entity.name : '');
+ }
if ($scope.isPersistent) {
$scope.currentEntity = entity;
@@ -2768,6 +2773,19 @@ quayApp.directive('entitySearch', function () {
var datums = [];
for (var i = 0; i < data.results.length; ++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({
'value': entity.name,
'tokens': [entity.name],
@@ -2840,7 +2858,7 @@ quayApp.directive('entitySearch', function () {
$(input).on('typeahead:selected', function(e, datum) {
$scope.$apply(function() {
- $scope.setEntityInternal(datum.entity);
+ $scope.setEntityInternal(datum.entity, true);
});
});
diff --git a/static/js/controllers.js b/static/js/controllers.js
index c2249773c..08bcca561 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -962,9 +962,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var data = {
'file_id': build['resource_key'],
- 'subdirectory': subdirectory
+ 'subdirectory': subdirectory,
};
+ if (build['pull_robot']) {
+ data['pull_robot'] = build['pull_robot']['name'];
+ }
+
var params = {
'repository': namespace + '/' + name
};
@@ -1148,7 +1152,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
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 name = $routeParams.name;
@@ -1452,6 +1456,10 @@ 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() {
@@ -1460,6 +1468,10 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
});
};
+ $scope.isNamespaceAdmin = function(namespace) {
+ return UserService.isNamespaceAdmin(namespace);
+ };
+
$scope.finishSetupTrigger = function(trigger) {
$('#setupTriggerModal').modal('hide');
$scope.currentSetupTrigger = null;
@@ -1469,8 +1481,17 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
'trigger_uuid': trigger.id
};
- ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) {
- trigger['is_active'] = true;
+ 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({
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html
index 6d2fe9665..f1ee224f5 100644
--- a/static/partials/repo-admin.html
+++ b/static/partials/repo-admin.html
@@ -270,6 +270,12 @@
Setting up trigger
+
+
+ Pull Credentials:
+
+
+
@@ -387,14 +393,54 @@
Setup new build trigger
-
+
+
+
+
+
+ Pull Credentials:
+
+ |
+
+
+ In order to set pull credentials for a build trigger, you must be an Administrator of the namespace {{ repo.namespace }}
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
diff --git a/test/data/test.db b/test/data/test.db
index af529991c..a2d065948 100644
Binary files a/test/data/test.db and b/test/data/test.db differ
diff --git a/test/test_api_security.py b/test/test_api_security.py
index 5fa026700..8c46e9b11 100644
--- a/test/test_api_security.py
+++ b/test/test_api_security.py
@@ -962,7 +962,7 @@ class TestBuildTriggerActivateSwo1DevtableShared(ApiTestCase):
self._run_test('POST', 403, 'reader', {})
def test_post_devtable(self):
- self._run_test('POST', 404, 'devtable', {})
+ self._run_test('POST', 404, 'devtable', {'config': {}})
class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
@@ -980,7 +980,7 @@ class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
self._run_test('POST', 403, 'reader', {})
def test_post_devtable(self):
- self._run_test('POST', 404, 'devtable', {})
+ self._run_test('POST', 404, 'devtable', {'config': {}})
class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase):
diff --git a/test/test_api_usage.py b/test/test_api_usage.py
index 977c20f2c..f03a48d87 100644
--- a/test/test_api_usage.py
+++ b/test/test_api_usage.py
@@ -964,7 +964,7 @@ class TestRequestRepoBuild(ApiTestCase):
def test_requestrepobuild(self):
self.login(ADMIN_ACCESS_USER)
- # Ensure where not yet building.
+ # Ensure we are not yet building.
json = self.getJsonResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
@@ -982,6 +982,50 @@ class TestRequestRepoBuild(ApiTestCase):
assert len(json['builds']) > 0
+ def test_requestrepobuild_with_robot(self):
+ self.login(ADMIN_ACCESS_USER)
+
+ # Ensure we are not yet building.
+ json = self.getJsonResponse(RepositoryBuildList,
+ params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
+
+ assert len(json['builds']) == 0
+
+ # Request a (fake) build.
+ pull_robot = ADMIN_ACCESS_USER + '+dtrobot'
+ self.postResponse(RepositoryBuildList,
+ params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
+ data=dict(file_id='foobarbaz', pull_robot=pull_robot),
+ expected_code=201)
+
+ # Check for the build.
+ json = self.getJsonResponse(RepositoryBuildList,
+ params=dict(repository=ADMIN_ACCESS_USER + '/building'))
+
+ assert len(json['builds']) > 0
+
+
+ def test_requestrepobuild_with_invalid_robot(self):
+ self.login(ADMIN_ACCESS_USER)
+
+ # Request a (fake) build.
+ pull_robot = ADMIN_ACCESS_USER + '+invalidrobot'
+ self.postResponse(RepositoryBuildList,
+ params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
+ data=dict(file_id='foobarbaz', pull_robot=pull_robot),
+ expected_code=404)
+
+ def test_requestrepobuild_with_unauthorized_robot(self):
+ self.login(ADMIN_ACCESS_USER)
+
+ # Request a (fake) build.
+ pull_robot = 'freshuser+anotherrobot'
+ self.postResponse(RepositoryBuildList,
+ params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
+ data=dict(file_id='foobarbaz', pull_robot=pull_robot),
+ expected_code=403)
+
+
class TestWebhooks(ApiTestCase):
def test_webhooks(self):
@@ -1642,7 +1686,7 @@ class TestBuildTriggers(ApiTestCase):
trigger_config = {}
activate_json = self.postJsonResponse(BuildTriggerActivate,
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'])
@@ -1654,7 +1698,7 @@ class TestBuildTriggers(ApiTestCase):
# Make sure we cannot activate again.
self.postResponse(BuildTriggerActivate,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
- data=trigger_config,
+ data={'config': trigger_config},
expected_code=400)
# Start a manual build.
@@ -1667,6 +1711,69 @@ class TestBuildTriggers(ApiTestCase):
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_robot']['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):
def test_list_get_delete_user_authorizations(self):
diff --git a/util/names.py b/util/names.py
index 7e48468bb..57fafdd10 100644
--- a/util/names.py
+++ b/util/names.py
@@ -24,3 +24,9 @@ def parse_repository_name(f):
def format_robot_username(parent_username, robot_shortname):
return '%s+%s' % (parent_username, robot_shortname)
+
+def parse_robot_username(robot_username):
+ if not '+' in robot_username:
+ return None
+
+ return robot_username.split('+', 2)
diff --git a/workers/dockerfilebuild.py b/workers/dockerfilebuild.py
index 9055d0152..9d552a4ae 100644
--- a/workers/dockerfilebuild.py
+++ b/workers/dockerfilebuild.py
@@ -92,15 +92,21 @@ class StreamingDockerClient(Client):
class DockerfileBuildContext(object):
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._dockerfile_subdir = dockerfile_subdir
self._repo = repo
self._tag_names = tag_names
self._push_token = push_token
- self._cl = StreamingDockerClient(timeout=1200)
self._status = StatusWrapper(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 = StreamingDockerClient(timeout=1200)
+ self._build_cl = StreamingDockerClient(timeout=1200)
dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir,
'Dockerfile')
@@ -135,6 +141,14 @@ class DockerfileBuildContext(object):
return float(sent_bytes)/total_bytes*percentage_with_sizes
def build(self):
+ # Login with the specified credentials (if any).
+ if self._pull_credentials:
+ logger.debug('Logging in with pull credentials: %s@%s',
+ self._pull_credentials['username'], self._pull_credentials['registry'])
+ 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.')
with self._status as status:
@@ -146,7 +160,7 @@ class DockerfileBuildContext(object):
logger.debug('Final context path: %s exists: %s' %
(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
built_image = None
@@ -200,7 +214,7 @@ class DockerfileBuildContext(object):
logger.debug('Attempting login to registry: %s' % registry_endpoint)
try:
- self._cl.login('$token', self._push_token, registry=registry_endpoint)
+ self._push_cl.login('$token', self._push_token, registry=registry_endpoint)
break
except APIError:
pass # Probably the wrong protocol
@@ -208,15 +222,15 @@ class DockerfileBuildContext(object):
for tag in self._tag_names:
logger.debug('Tagging image %s as %s:%s' %
(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)
with self._status as status:
status['total_images'] = num_images
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 in resp:
logger.debug('Status: %s', status)
@@ -245,20 +259,20 @@ class DockerfileBuildContext(object):
def __cleanup(self):
# 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'])
- self._cl.kill(running['Id'])
+ self._build_cl.kill(running['Id'])
# 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'])
- 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
# doesn't know about, this should preserve base images.
images_to_remove = set()
repos = set()
- for image in self._cl.images():
+ for image in self._build_cl.images():
images_to_remove.add(image['Id'])
for tag in image['RepoTags']:
@@ -278,13 +292,13 @@ class DockerfileBuildContext(object):
for to_remove in images_to_remove:
logger.debug('Removing private image: %s' % to_remove)
try:
- self._cl.remove_image(to_remove)
+ self._build_cl.remove_image(to_remove)
except APIError:
# Sometimes an upstream image removed this one
pass
# 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:
raise RuntimeError('Image was not removed: %s' % image['Id'])
@@ -338,6 +352,8 @@ class DockerfileBuildWorker(Worker):
job_details['repository'],
job_details['build_uuid'])
+ pull_credentials = job_details.get('pull_credentials', None)
+
job_config = json.loads(repository_build.job_config)
resource_url = user_files.get_file_url(repository_build.resource_key)
@@ -378,7 +394,7 @@ class DockerfileBuildWorker(Worker):
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names,
access_token,
- repository_build.uuid) as build_ctxt:
+ repository_build.uuid, pull_credentials) as build_ctxt:
try:
built_image = build_ctxt.build()
@@ -432,4 +448,4 @@ else:
handler = logging.StreamHandler()
handler.setFormatter(formatter)
root_logger.addHandler(handler)
- worker.start()
\ No newline at end of file
+ worker.start()
|