From 2006917e03750ff2834de3ce064f30331e53e0aa Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 27 Mar 2014 18:33:13 -0400 Subject: [PATCH 01/58] Add support for pull credentials on builds and build triggers --- data/database.py | 1 + data/model/legacy.py | 29 ++++++- endpoints/api/build.py | 37 +++++++- endpoints/api/trigger.py | 44 +++++++++- endpoints/common.py | 4 +- endpoints/webhooks.py | 4 +- initdb.py | 8 +- static/css/quay.css | 34 ++++++++ static/directives/trigger-description.html | 2 +- static/js/app.js | 16 +++- static/js/controllers.js | 20 ++++- static/partials/repo-admin.html | 46 +++++++++- test/data/test.db | Bin 194560 -> 540672 bytes test/test_api_security.py | 4 +- test/test_api_usage.py | 94 ++++++++++++++++++++- util/names.py | 3 + workers/dockerfilebuild.py | 46 ++++++---- 17 files changed, 355 insertions(+), 37 deletions(-) diff --git a/data/database.py b/data/database.py index d99f56c77..b0cb60bb6 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_user = ForeignKeyField(User, null=True, related_name='pulluser') class EmailConfirmation(BaseModel): diff --git a/data/model/legacy.py b/data/model/legacy.py index 00d9c9b26..116a39a96 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, @@ -1449,6 +1459,20 @@ def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, display_name=display_name, trigger=trigger, 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): 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) -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) trigger = RepositoryBuildTrigger.create(repository=repo, service=service, auth_token=auth_token, - connected_user=user) + connected_user=user, + pull_user=None) return trigger diff --git a/endpoints/api/build.py b/endpoints/api/build.py index f14d097bb..c08ee37ec 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -33,6 +33,13 @@ def get_job_config(build_obj): def trigger_view(trigger): + def user_view(user): + return { + 'name': user.username, + 'kind': 'user', + 'is_robot': user.robot, + } + if trigger and trigger.uuid: config_dict = get_trigger_config(trigger) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) @@ -41,7 +48,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_user': user_view(trigger.pull_user) if trigger.pull_user else None } return None @@ -88,6 +96,29 @@ class RepositoryBuildList(RepositoryParamResource): 'type': 'string', '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'] 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 # 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) 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) repo_string = '%s/%s' % (namespace, repository) diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 1eb7cd169..0d846d889 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -15,7 +15,8 @@ from endpoints.common import start_build from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, TriggerActivationException, EmptyRepositoryException) 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__) @@ -133,7 +134,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.' + } + } }, } @@ -154,7 +167,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_user = 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, @@ -185,6 +218,7 @@ class BuildTriggerActivate(RepositoryParamResource): log_action('setup_repo_trigger', namespace, {'repo': repository, 'namespace': namespace, '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) return trigger_view(trigger) @@ -214,8 +248,10 @@ class ActivateBuildTrigger(RepositoryParamResource): dockerfile_id, tags, name, subdir = specs 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) repo_string = '%s/%s' % (namespace, repository) diff --git a/endpoints/common.py b/endpoints/common.py index 4832aaaf0..99ca4ef1a 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -100,7 +100,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_credentials=None): host = urlparse.urlparse(request.url).netloc 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, 'repository': repo_path, 'build_subdir': subdir, + 'pull_credentials': pull_credentials, } + build_request = model.create_repository_build(repository, token, job_config, dockerfile_id, build_name, trigger) diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index d92e7095e..69ba97b58 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_credentials = model.get_pull_credentials(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_credentials=pull_credentials) return make_response('Okay') diff --git a/initdb.py b/initdb.py index a4b1709f0..fb55ca8fd 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_user = dtrobot) trigger.config = json.dumps({ 'build_source': 'jakedt/testconnect', 'subdir': '', @@ -354,6 +356,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/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 87f73c724..00dc325ac 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2504,7 +2504,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; @@ -2628,6 +2629,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], diff --git a/static/js/controllers.js b/static/js/controllers.js index b74071928..2daf27c30 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1158,7 +1158,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; @@ -1462,6 +1462,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() { @@ -1470,6 +1474,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; @@ -1479,7 +1487,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams '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; }, function(resp) { $scope.triggers.splice($scope.triggers.indexOf(trigger), 1); diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 6d2fe9665..80d45768e 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -270,6 +270,12 @@ Setting up trigger
+
+ + Pull Credentials: + + +
From 2a72e91bdb166564b7c644f61fd861e2f1e9a95e Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Apr 2014 19:33:11 -0400 Subject: [PATCH 05/58] Prevent the entity search typeahead "no users found" message from being displayed when the entity is set from code --- static/js/app.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 7ac0219f2..311b531fb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2728,7 +2728,7 @@ quayApp.directive('entitySearch', function () { entity['is_org_member'] = true; } - $scope.setEntityInternal(entity); + $scope.setEntityInternal(entity, false); }; $scope.clearEntityInternal = function() { @@ -2738,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; @@ -2854,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); }); }); From 38cb12b7c6706b4ad2afe26d48e2816a8f60aa1d Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Apr 2014 19:44:31 -0400 Subject: [PATCH 06/58] Make sure the sign in form redirects to the landing page --- static/partials/signin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/partials/signin.html b/static/partials/signin.html index 434d835d3..4aac6cb7e 100644 --- a/static/partials/signin.html +++ b/static/partials/signin.html @@ -1,7 +1,7 @@ From 9a79d1562ad2aaf8452910cec82a5db33e19b940 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Apr 2014 21:49:06 -0400 Subject: [PATCH 07/58] Change to store the pull robot on the repository build and only add the credentials to the queue item. This prevents the credentials from being exposed to the end user. Also fixes the restart build option --- data/database.py | 3 +- data/model/legacy.py | 31 ++++++++++++---- endpoints/api/build.py | 64 ++++++++++++++++---------------- endpoints/api/trigger.py | 8 ++-- endpoints/common.py | 8 ++-- endpoints/webhooks.py | 4 +- initdb.py | 2 +- static/js/controllers.js | 9 ++++- static/partials/repo-admin.html | 4 +- test/data/test.db | Bin 198656 -> 544768 bytes test/test_api_usage.py | 39 +++++++++++++------ util/names.py | 3 ++ workers/dockerfilebuild.py | 3 +- 13 files changed, 110 insertions(+), 68 deletions(-) diff --git a/data/database.py b/data/database.py index b0cb60bb6..d6a67bd80 100644 --- a/data/database.py +++ b/data/database.py @@ -176,7 +176,7 @@ class RepositoryBuildTrigger(BaseModel): auth_token = CharField() config = TextField(default='{}') write_token = ForeignKeyField(AccessToken, null=True) - pull_user = ForeignKeyField(User, null=True, related_name='pulluser') + pull_robot = ForeignKeyField(User, null=True, related_name='triggerpullrobot') class EmailConfirmation(BaseModel): @@ -245,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 a2cac1d26..3cfa6c27f 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1453,27 +1453,42 @@ 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_credentials(trigger): - if not trigger.pull_user: + +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=trigger.pull_user) + login_info = FederatedLogin.get(user=robot) except FederatedLogin.DoesNotExist: return None return { - 'username': trigger.pull_user.username, + '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): return Webhook.create(repository=repo, parameters=json.dumps(params_obj)) @@ -1530,12 +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, pull_user=None): +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, - pull_user=None) + pull_robot=pull_robot) return trigger diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 3ff6dfd9d..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,13 +35,14 @@ 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): - def user_view(user): - return { - 'name': user.username, - 'kind': 'user', - 'is_robot': user.robot, - } if trigger and trigger.uuid: config_dict = get_trigger_config(trigger) @@ -50,7 +53,7 @@ def trigger_view(trigger): 'id': trigger.uuid, 'connected_user': trigger.connected_user.username, 'is_active': build_trigger.is_active(config_dict), - 'pull_user': user_view(trigger.pull_user) if trigger.pull_user else None + 'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None } return None @@ -75,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: @@ -103,28 +107,9 @@ class RepositoryBuildList(RepositoryParamResource): 'type': 'string', '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' - }, - } + 'pull_robot': { + 'type': 'string', + 'description': 'Username of a Quay robot account to use as pull credentials', } }, }, @@ -154,7 +139,22 @@ class RepositoryBuildList(RepositoryParamResource): dockerfile_id = request_json['file_id'] subdir = request_json['subdirectory'] if 'subdirectory' in request_json else '' - pull_credentials = request_json.get('pull_credentials', None) + 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 @@ -170,7 +170,7 @@ class RepositoryBuildList(RepositoryParamResource): display_name = user_files.get_file_checksum(dockerfile_id) build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True, - pull_credentials=pull_credentials) + 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 fc734d3db..c62367f52 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -190,7 +190,7 @@ class BuildTriggerActivate(RepositoryParamResource): raise Unauthorized() # Set the pull robot. - trigger.pull_user = pull_robot + trigger.pull_robot = pull_robot # Update the config. new_config_dict = request.get_json()['config'] @@ -224,7 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource): log_action('setup_repo_trigger', namespace, {'repo': repository, 'namespace': namespace, 'trigger_id': trigger.uuid, 'service': trigger.service.name, - 'pull_user': trigger.pull_user.username if trigger.pull_user else None, + 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None, 'config': final_config}, repo=repo) return trigger_view(trigger) @@ -254,10 +254,10 @@ class ActivateBuildTrigger(RepositoryParamResource): dockerfile_id, tags, name, subdir = specs repo = model.get_repository(namespace, repository) - pull_credentials = model.get_pull_credentials(trigger) + pull_robot_name = model.get_pull_robot_name(trigger) build_request = start_build(repo, dockerfile_id, tags, name, subdir, True, - pull_credentials=pull_credentials) + 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 e22df6226..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, pull_credentials=None): + trigger=None, pull_robot_name=None): host = urlparse.urlparse(request.url).netloc repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name) @@ -118,18 +118,18 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, job_config = { 'docker_tags': tags, 'repository': repo_path, - 'build_subdir': subdir, - 'pull_credentials': pull_credentials, + '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 69ba97b58..93d5e413c 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -73,10 +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_credentials = model.get_pull_credentials(trigger) + 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, - pull_credentials=pull_credentials) + pull_robot_name=pull_robot_name) return make_response('Okay') diff --git a/initdb.py b/initdb.py index edad2f2b1..854f83f8a 100644 --- a/initdb.py +++ b/initdb.py @@ -332,7 +332,7 @@ def populate_database(): token = model.create_access_token(building, 'write') trigger = model.create_build_trigger(building, 'github', '123authtoken', - new_user_1, pull_user = dtrobot) + new_user_1, pull_robot=dtrobot[0]) trigger.config = json.dumps({ 'build_source': 'jakedt/testconnect', 'subdir': '', diff --git a/static/js/controllers.js b/static/js/controllers.js index e907398eb..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 }; @@ -1486,7 +1490,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams } ApiService.activateBuildTrigger(data, params).then(function(resp) { - trigger['is_active'] = true; + 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 172b6d9f8..f1ee224f5 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -270,11 +270,11 @@ Setting up trigger
-
+
Pull Credentials: - +
diff --git a/test/data/test.db b/test/data/test.db index b52ec7fe126611a7256b4d1fc9673b523ab1a3d5..a2d06594804813bac40b14a63510edfab6653493 100644 GIT binary patch delta 18485 zcmeHv2Y6IP*YM8V?Ys95JwPfUkc6<=-l*C1LOLl#Fzjx2lR!d35<(N=+7$uuiUU|c zQLzE~qDE1vV#kgY6;Py$6vgi6f9~A?F(JNwpa1(m|MUF+KIAgx%$aj%&YU^t%$$4u zUE1r{3>jHmIH9)17v*ngs&}_UnI*j>2uYHnq9hV#sYD{V8U8N)W-c3PPFpI`eH9~v zA{jX@R5X!a$q(c_`Gy?VEYkE-eW$uz6{fsbIasklQ6oPmUnQF;{YCn$w21kgS&tjg zkEm6$ZJ8^KPrWG>h4Wul_2(bFX|k#h9tDQ-%c}eF>u*x?iPanmNx%_UjiDu>7uU>3ddWiW&HUYI5vULUY}zD#LfT-|o3Bnw;18z`K)v`M zQ%mqXj=v=#kxxz-#Gf0X;y+Co!2}qVpOR|i_l_9EuS<3EAqfeH<(H-oEUK5j?CectnkaN914@o`_tOkc*poTh20@qAZ6y zJ@_Q;Wd5R}myG-%AwTp0F@^s@!=*ChJfM=?7-RvK(XP`fG=BBB>JqhF^`uIzoTj*- zn50n4H^@0zz4W4VvXo_>WqRXA^b@K?y(G^`ZsCnumm;x5z$2mO7vdQDN+~Xv8w0(@ z=0;Go1DoiCG902XKBsy)Fd&>3?!adHKpC!-DvwX?PZy=)7BVs<=U5;jR3#~qn1=*( z^qDxEK%Hs0nuMU+hX%q2chn@%Arxv3q+!3@y7J*Q6@%y#C$N#uPRGNf{W2eEq-)Y~ zj?|cS>r8q!9WR$h_f7k(bpSnc5`bRLz%IqGKfb*o5YeCdPXS$Y46Y%gFT8pn5Z>=f z4`oec)3hXsLfa_LMdg>LoH4Layy%v1MdwW{fvDbQ+uB2JQOUWjwht%>*nDVmQSr&IO# ze!1kyyL1%_y5(K~`YaB|$j$RTMY(bsJ`oD+@i6|fIF~jsKt}5)!ua17592@n7yaAx zbUbcR*thlD6o_Ex!V;)kIRaNJh9@b02!vxASqdei5@5UynI|_^AiAX#5bjIBg9OgJ zehNIU8wZd1kpff3XkU40OV}A~9&2YKS*7;8_JH<AjbS7TE+HH;lYwn)e)Y;Y}-4@yZ`N~u&yy6jI9KIn(dY~+w zKavxzB410$kK}7|rU$4D{_Ff9Dso7`b&4G70V*klV>fZWrh081`Y-rTtoaqc%r#T625n#sgO|blgT9o#f0jR%B4DsosSr6gC_3kqG9^ekstT4s7lS#oY# zxod){(3+KM%S@#?kK=j z$=4g))4X+dzco3tu{F)$w>u_f8bg)TtlH_ic}+s z49H?t5~Wh&!Q)Ya!_r9uvX{PdI=neLkE;@O2Z+V3fBO)_O9r<h>vP(jKBNxaS za)cZt`vLxaSa2uE+hkYQHiGT}B5?q}%R9QGL$UFXV)GN15kOP&e$R~?l0wNt5^|Ir zCZCZ{$%o`!@&?&K9wQHdG&3rbz=V`X4WJMaN3#-=0>J^iP?eZ<=nc{ioc9Ll~=S zTC=yNp{XgU%GcWB_SE?jy$$t(9F=nWB(SO1k!WsT_aPm`v}H==V0Q#qE&itio5=(* zY4CqzaNDjbMl*2`l??yW5za(GOc-B%m`DBnRV^$^DLE+to%D z%AaW)sQG)aGXC57AsX&W33&;)Kc39&iZ@9c&CoRtEN}bAQzXrh!BnIF&IEOupSCOe z9Mgg5pH2@>7YI`IFgac3CgCqcZg6s75Pt@X_$2wV>mYVaPBQ=HqM>63qaP*cN4$*b z&-^A0NKeS>WuM9G2SnTOo+}1pHqva`5Fd{&o0dgaOwO(b=~Zmj%LZ+OXy9gv z2Jd{)0L~UI;#}0pOulSBcebj>ME(sk{NEb$oeY26+Qli5Du|grXqE)cLO4TfEMJQSS@G~t2W2o2*O+SrdzTs;R3??`F*TUQhQz5R0jv(>}+u}|w!?56ss!!e2j zfJpwy4O3A(q@wt<8#KIa<1{n^a+Um~ZR7a5rcaLf;%Q5H*&|iuA(Gu z0>d9$)jzJ-V#-L*%gxPA&aQA}nX__BQ!-o?#p%V)wCq%Sku^JeQf5U#S}wo!zF~ak zefe?arktW2rz^`*o}CTq^`uOTCAqLH*O2TgNVTVBSc{U4lXC4DPH5oL3X2&FmfZJ$ zWy!4tOAcHqAdS0{*|FFwL`nSfI<1U640^`Zpq3S=Y9=F%q(r3}(m%w-vSypbVe{F& zI={nd(pl_2o6h01San8^xyn*iRpquitxdkhhEeXS`q~*SzUG#ehUvZ;CWFzUGr&Jn zl)*5{Y#L>7a+Ul~?W~Oa z20CC55MKVbmsS59;tkb*#9{@;Bda823t82j{6h@CYOlY8HU7@DAJQrWKiY3SdeW{D z+2fZZ8U;Be8n4|u6*c61{?zeM1%VauJ?XY0IwmoPKXy7|Tz|Adf;Qk2OaZf+IV$zb zys~%XX89Ing7Q99Z&g5bLOV>mo1M=oxo5~I5(8|RED@YA!cR(lE;Jlv&^ukoPLC&{ z$b>m*ijc8GP#{vMZZm?P)_;RtrwHlaAKg?as(CFwcfIgu;Jy1^8`x9jChxvXgpB;t z`>#bNUUMJ^nfbB5^Wk z5RDVkjc9y(VQ-X+Q4*ax97WRyhanYxyDzfQ-7zQ=xq_wVVo)ENJq($!dK`UbI2whL zgB2ePM+(|67RBOWrf-2=ir{`4P%Wn`U%th*Y-vI5Rqh7^(#q|&?m4!a%XmqX_;m_dMf>`tB2 zVFm%_a60{No6+d;x|#XW%?xUrESUJs`=73uj!ae@De%Nl}|1MgPvr2Pe8-0+#WEq`n(H z1D|1NeJIkj87*d;*J7~is*E$MyG4Ax_%1eK`3gi;%; zs%$>1(V+9XjY0#5UFY^Y-8vs^9VV;K#gc%V0{UFND_bALJ(({q1pDGCUBRSvgy@J;ML_y{aVi+l- zLYQGxsvz>S5-@0b-bjm}m74x+Mq050;n1lkZU8zG1UdktXeRYQhk#K^ztf{WDgw;^ z%U>}+J#9yW6@oE!u}7mTkDeTj!bCxSs>dDlF!Gkqg6D%Y$i95rs=Q7!!S)Mg% z_iJZrODK`z>~z#cKBGJWi)m=kWSH4v*7qsdD=0%v6*b z2^6m~SWDpNgz8&MK?L3O}8}=dtQ6kk)z3 zpmW&Wevb)$Ua!+kO=)OsjLq%vgB#MJ14Krh#cXxz+y;+B=K*X+P)qzqhnw(g1he!tG`_4svmhudH=S>0}r*G7L%LkTe!yVp?V@jHM!4K@%HPM^-> zt}^SYyjFwFVRPH8RaG=I9l2s`R;$A=^yxQLIe|{U5nkKm&;d5Lv&!goJMDJ5J{^U{ zn7wAF#csFhJXQ

F|1WPLt3(fEX-Bv)$u%(lhBOGsaw1W$-u+UY*@&g;BEDVU%pJ zUVKik+vD(AoOY9$PRc+dzzJw{8|)6N&SZzRXR%rBIv7WT4i=in?{}CSZnKB3%Ru0Q ztMZ!t9~p1*(w(m!12h@eYEqjaiv zp+Hc*sOYF}hiW}SS@cv{x(2a5HUp*&-Mjsa5vB3Jyr?*mY`#SE8E_k4aUu&| z;LT)f_r`~rWd#bl`5KfHNmdI&|9fyg9|MQ=KH&Jb$xiYLd7f+_tI1>C+rG=t)eXoW zNmfe88rTB9fkAqmY$Pjz?me)~eu3fIeHEQ&;Oh)s*^KIBLc^8a@xnN+oQ*VoOG$Bd zs--BkqM|U{Whl!oDXTE%n9C>GN(;+VjCm=>?0j=xsi~mE?kYAJ(+e|7QYYAK<;ifM zQJkNaSEB5$kXRE94yVyz!@^J_^0MUW^9WlOm#x^)q9 zX*$N>pP8HVx77$N(!Zwu-@YJ`GD!6|=%K%@Ln;-UA|ZpgL^h@CU?gd8LA?Ss3@hl; zC4p-~T|-n7ddDUdL+3t$9*pnMjls4zS9M@?^lF`grHlDqGYSBcUSwwN1f>IN{fQS zy9sGgbpEF(R11xyfskp6km3+T%@<&kXryhK)!@53dBd zzhxzocK{@9gxw1Ydv)Yf??Xjsa@Phk?gJZf_kGB$zB#&FA>8P>eLjtR8uq^_L2AjT=h!i(z-DTMTYK=Xx#u_-QnQ2}BmtCs%>_Sl$)wyQ^R~3%?KPaKc3T+e(yy zCUpggScM3ky$TssaV39x$@xp6_lmCI^Ohn`*r9sS)!Tw^tD!fyA@YCs7=5UTZk$zD zS9y7-tDxuBqdD|cAJj{AxklXbXw3#>LY0@_TTWlvfUZSTFBi-~?#pR8y<;N`U&9_G zrwfFEocc5P-dFlaxj-Hup#Xd5D^>N2#FlI_SG zQK<+C4@bAn7Z*=mL$z;4OViv-|NGjfZbx?9Va(qGzCLmb+1ORML|NWq=q;O2+uyN{ zeg%80U9h(zC%Jbk2f!d)vcq0h>4>H7>(senGs!Up~Yb8 zKul`a3JT5Q>2v5RltWEQS8x$eHf9pKRtR|F=+l_db*njnnbNL>fTut8FpRNl1$~%d z#w2$c#QD*JH&AfwH2K_BzNY5r1%d*h71X-Tmu;RSpl9s`_3Ry(uO3u74c+`c$_XXg zB>?w2sH;~L*>8XsJ|*vTnp43EddXVqL$`m7#@{;?NyD^)dL*=a85{k|%GSFRz5Zp$1NpNNaUvDeC;7QP8zW{Z* z2lvLZ0=209zIp^@hH{I+MFl$RBXAqRPcldxQE{iZcetmyt6aI*g6ZO8h+NvSFR`39 zf^nhlisPkf&0klm|GHY$y}C{C)?LD`>|WowT5+Y?tTQ<-`T8!kmj9{R-nCkG)#|p( zolE~zZ|mBe`Ri(Y#RCYmb!*+};`7ZgmmL?M)koElsj7#RzbcCqk0~VbGTBoyEOj%l zG6QfO+KL8BW`JpIqoq0c$@T$B_!QIri~$!g?L%VlA*S6JgX(PG!7ukW!`iS#eUaU-1@r{u2q}c4<~<#*s+Ews9A@ z54pqK3)~~zQf@XknH$H2gInq&_HlMTo5K#&ex-dvTd&n=PH7&|+^(6e@o2I&dQB8q zguBQ~n%)|j`Umyb>i5;{>ig9T)l<}Y>SVQ9Jw)9HE(8ClI;#3q^|tC|)zhjqs%5Gh zRIRFNRf#G|6{ljAXO-`fK~I74)k^M$8<#aCiSXuYB~x``f4%n@5meUJ~3a#F+-5vY1KPzdZ$D0)i9x#vYt+9 zx02~EmQNL1RR*(W{k39tN)(euhwR0JrB0)MGL6`aBbdRAQz#0NR)jESA=R&KQa?tA z^bWJ$VbMFRdWTK#u$)H|w*5-CQck z3bqxqnZdROeTLXJU2JQ!>TNc?&91jO^l4&Su_Q_;N);OiAyUM~kWLmGyXc0qXb2O| zjHl0@MT4c|g&~Ag2%H3MY{moI#No4A^;VnSYS&vGdaF}EM(nY3;b^hP%O#`44ue^z z*de4HVuyAS5_r?-)LTt@+n=*mv0<>*0&|FHzh>MoLnNZ3)ULNU^cJVyBofiN(AWu= z#jZDW!lf6llu@@ETj=g4%+kUpoI-my;Uwv3Wj!o!451Ir zz!4D!eg95{{lpo8k?SkYNFVz6UK}}yWg=m*>5T@x(V{16W>80tqqpzHVQoge_DY#X z!SofX)gl6wh#UA%Z`Uiur7oluB8*ip7hz<<{`DrkSuYKW14uI>j8TuJ3{mMtVa^V{ z;@&z=zJx1_dq9f5)dqY1t8g>@S-52RDEO*Y5}qt3w}M2RPiBz@QcJvK3MnInB!{Gt z@x%$2GIb=D3;~aDe?o`~?y>*Q{m7l?PIE`OL)?DuV{SM12DhDinR}kw$UVWWbYvp4T^FJm(O{#xD;+IXXlJu0ym6{^pE3ek=PDyNO-Tu45l!?_-y?F3B&1ExK z7dx7@vU)b29oj|X0uNQIp!l@MUPK3R8yh7I%n#(;)t!dqafl1JpWF-n?mNg$WFdII zo8VHRpG+kc5GRmFGD$KSLu|xAM!>mt1Q|&B5G|1t3HJ-!>im{F!F|Pj&V9;#z`f1A z#%Q> zi+BI%gSX&JnwXE5QGGR}&Q`%=-2`~Nk&Ex5Z#W^<{0DL=f*13oG*F9^=vF6I(#mN# z35}$NT%1pj`vI;@cx=tbF4|ZPWwFqVZu3E^%?B0hZo(O~G!N&{lX*Cu+J&;?qu@b= zW~)U6LU!wD0l{c!Mucozu25DDWhW*;**bx~)p^2W6wacC(SYih4N|LZP-b^RA6x!_ z6a6?lrgWVH%B~*22hO9M0QrU$N?lRHYgpkm%4~pptp(pGg~4f~%FQ@aNG+rW zps-Z%j>WRiOTgKF43xFEVL7h{wT34*k`_`8OqxrQVA00Fa+SkEJ;i;-y$5W#fqRf! z418AyTGs>!Y#GUga%#}MPQY4woqfK`n%R`3eFn8Zk8lniDRkIJ)+$kcrOHulR*zIa zqG2?Cwht`84>=w8hH|>%YelL&SN656GqvyJg#@s(VxJ{O0|#;q1SzabjU-W z`~;0aHNm29J^>G+mxk>@BT=nTG?;q#AOm<=?%4z0vS~r6_l2~Nezyk=L(_vrhELJ( zfj#*x+h=@=?ncpN)$)*p5Hw$;T=ZyGx@UOdVpc;_vzV<9dMMKc&tzu@WfIWR#f3Rs z@mEeylfpJ#5fTeS_ud6G1kG%RBeuS%D3*kD9CmlunwL#$usSyZ8?J-9@b|%)-%@4jkTBGJpupa-J@%S+_wmCXBL_^RlPe zqW?*xCm6sp{(ora{|lj=G1>i|+5bq8YZ@eED)4#+L}*q5*LXEMG}-EB)gEvO_?5eq zxr*l$K7~qND|=H`AZ?e{NVQBIeg_w$&1gCzk_Otd=h94l-Q?0)T!cA8&mqGCH$`*JBW~BFIg)tZ! zY@Y@15YqIun^w*O%$7&P4$uv=0JAZ2{r#a7IH7yz_p;Ko= z;I3><;?9bMU8*RuG)3{&Ok&gUASBb+`Se8cO&}RG=cEpH1b{{aJLd@4VCVdR}2e=2Jfo`TBG;= z@H`Ej2(*U%@xv2z+C-qWPjvO%)@XVrUg)(1XpP?%{~4WM0<=04_CG_zO7Z=ptO<)) zfh|HLlO-gQn*;jt5baFO7EOq{UbRydtz4+stI)}>mwg}$msT?y@C96m)()rRIGL@`AwnBDT z9w%>&3HQ-tL30zONsXkB-R8Lhuu0E%6Xs*}1sqLjL*RIi?U=8dX zb{pJ}Er2-S!{C2yBro<{b6J$q1PkWcr&#b>MkXr2!8CpG50u#sp)*q%S|=ta2+1f& z(lu51t`4#d66qZ2HktB&AJJ07-zHlns`s$9e{elcYTtS@-1(um{TC2eem_1)7rulu z+ZX!q4fwKRA5`bt*W4t$iU8RTkdHHTjL>M!i})~o^Cg_rv3GzJsD$Y4JoXQEEl2?m zxDX5B+)|T_b@+RH4c4M(;41;56ri4M_yL9U zt2f^Ygij8N1Lt;N&;I)LA5~lv6bE~^1AC6EuCJhfYzOv?YCnc)-VT91>1#i>mIuW_ z=uTiyY3TPa(aN2|BF%iYF%VvQWswSB2w3?XV3r(NzN(@mxcqI;!}7;RcORpb&jaT0 z*FUn*4bKDS-lMY5wH5`J|GXDq`G=;ywwG>w0T>?-n0g1D+K%s++7^v&rt8+gXAQXA z^*OnD6pa(s$wRO@?Ue_Q1_tEP!g0dtd<3V`P1HT$`I2lmhwpKtI~v z#+88q89^zq<{n%rkGg%%8?EWWcL6Naxg3v^hQEE_1v-B@P(JXn6&JJAiL2h#Jg8&mKU>xwqf^&fZuHXj&9-u(EHR{8IbkUtaIlA?lt$_%*7cG1c zz_}P#k3kO)dn~y2-xm~>e{mdJt$kJ7OVgl!M;)V@r`)f!DwfEy)1%nLY50$u4V4Qzv8EH0dP5imwbm-{sBBbGE8?g z5I#@fahUdV@SO+qng<70%oTVXzVmP%IB}fIGJsZ|2To+&^VZT0=YbPP>(<}c+A8q4 zk|Njy-|uW>uf z=S*6-5W2nfB%UgD-jkh5PoKms(%}b>Mp5r6ST45P>@x%5Gp;O`GT=DpY2jlOdwPFc zQ7`OiM!NSj>_qHHoSIuF@Vb%aeGWZ@pV{mT445wPx{0p-98Z-GZQuG(>oj4D10ucv znq!Z>zL(Db0%-2#a-X4L2Z3gV3%^BQIS4dc-gtdyu3zA4Bh5Jk7{?zi*%=t%6S&$$ z?>_`I4}UnMoGyA7c+i>q!2UplH@LSscf;Nm|4QS+K)5Hkw;k9m?8A>V$5*(6d)u7% zKzNwvEeg;z@4>*Di-(M&XWxT?wQVf=ruEw3-uBA-IH_cG;_C;6t!9{Pg+$m+6q&+? zqL6gnQeeZm>&mvG*e7rN^9CY2ENm|FxZw68TS4d$3my~v6br;RA!;~^6_oq}9*Awn z!TGS_7r`@f6nqh7AHiem%ixAMycxNQTO2&F zsD(-|! za2`&>_iz}#fiK}R_z2#Ex8POS30q(ztcCx;6Yv-;2L?;mcTM=2cFY-z{o0)fCN&@! ze+z=<8U#&M2u4>TxTOrih@l873lI#=Mvy-k!4L}qvkrlFAcBF(2>SOykeGlVItD?v z9teU$5CkYCNZ=fYe^-C&V-mgw&$;+81)S&LJks<%c+QJm26To0NR3|l-z&mC^Ppq< zENs1X3WCk!5o~Beux=EB=SLuTx*Wk1MF{?tg*K3;CM7 zN}eYR$plhH%p{(uxYOKTZj+~Lf@}Sz(sC%_yd3U~7mmVex&h+o9W(=gzDokgBAA3* z`2{J5eEJYQLK5gWaD$1okzZ*F?Mc2NuaPxS4UL?a4kH%s3=Z17-aE1k1UQVGg~P~> zFTiuSElz+V$Wb_g1^d8rq@!1ul3T!W3rG@qiY|g4FdO@}dtW+$G&%(y#APjVl94kF-A*7_Y;!B!q3D%R}~xt|2HRfk9u z3GpfplN924TNUNm6_Cs(93iQmq4+U6Acf8f@$^!qkuVl^ltf~6fM>jFK$k#y+8rEx z498&yPP-O7J60AF9?^@ibgkmAVW%W2>pA>8KuXC{8c0XO6S&4aYdS`XNl#CxQ0IAAnMlGt+m(7Y>NtrY5$ne*(g<7qJ?>*9 zBd4*IsOanG)Io{C-@`$q^AqqKyqp*S-*WI1d17Q2( zNxuL%AhkOV2e9lzU*y`WFUTYwO!lg|XxZ61V zdkuGUDtQZK=WY)Qr)M1>L!vzI@@iTZ?pXs-B<9MB6-@I3iKe3>SkVd6oAhF1Pmsa5 zg_j|g#Cl?Rqo<9glDPGc&~AiwM6--jL{H+`q*J7Xw#TpqCy9n6NT0Fnvy-HMg1jo4 z$S=ED(smVOPP>S>{5b=*Z5`uFJ_QuODGE*VPqjaw~=xROIUQ zHAN-*g3|1=>Y>`~`l=dRVRg1G&rnceuoM*OwK{{LxU9M&e}v5#oX@*MNL^XAL0?vz zlU-DxwdM^SW~t4tuhi#P4YTHGbBnX9Yz0Nt#pNcg-j-ccHzKc~u%t?pQ=OM<6)MXt z;dh=E9o#GP9+=q_VoU*jQX! zJ=9WSsm!gbG@6WshG9A8n#w9Rc@Z&o6cpr}YRd8p%C!0A=31SlxVE$;zr>VXP+p|T zE3c@uX-s)mO-@OkNozFcm<%Q6!s7CLy{^z~E~vF-SJ4PC28^B3IHqm#1Q0O!SRqtKf|gwX-5&SMEm3p38sH{i;BQEQV{)rY*WXz z`{duIBI+ATLI3Vn6%pS^l>YH{rT_K)lD371qZ}Hub8r%lBL6-IcB%su?1!BsBIw4d zHsINbH6${Ke$C;jcoEG)3ikSdb`&a*<>YU(ocE9O|6sVx?~bbn!_RQ=5A=80O$(%j z1^fxKuku>|KM#i_tO_oV!@rBEnh52^& zYYu)!x9cc;E$tDoulJKal>2k@^`M3-1bT>rJLnY}=#<#|?vrAS+tUtMh-UYS>Kt*afHt1r)CJ2PotR+LH0`XPlVTBH|8deH;Wj5Bv3?1ghg3`xF*18o;b zxQ_$L=gHlKq_B0D(MZ1f6PZB<`0#EQNHojL10|buf%FZQ*gz!*?WPnu5%bBJ14fK+nZWjCQNlsnL4#?oO6Osqcx~C_@_N5dCRuNB861~EYv_A$tEX#%nmu@dh$Pkf`UMh8iQ2cEF29f<2A9Zal6JKqnB94a zOk-;=lb#IgOJqw-0JWf`|K0;#GE@ir6_O?95V*j>1?=|&unUW5hys2=TaDjmfc>(P z23+&KQ=vj{fCm99Kf-lN0C;-wuG3o7wZ(5J61rSQfm>%6=lrY~FrFSN6qLz9;)M z2h7C6LUZ9(Vr3I@p@gKfEx9m)3}RjLUj|Zo)>>F86 zhxykwjN^uJTSy{#4jq*RxV9nGrDyNvLw}ON7Un|~3oU>o_Kt+v3P1>&>a@4EI$Isi z$(d4p2GbXSo@7Z&i(>N%z)7+_TZ5uE6+#^$IV`IfV%W4ITz#$&KU@S?{NwA0YJ;8u za9lcHKf-Y&^h;pJ7eTZFp5)+3{9XZ#HALX5Nr0H$sI?mmc59PbXD~IY4H{#U+ODyi z)q0n~Zq_;U8okj7vlFMZwK@~i6B66)5Mv)!QzwXOE@`B zbQ;*DH4u1>1j`cr4F}&~{b!IC3)umGKAP{8$_je}iGf;A*8~IWS`29WF;NgP62uOKBY{qRlgXqpJJqfxlU8joJ6&pv!(>$(tp-g~ zgwAC#Sd5JT5~Bf5OX7H17Vv|>PG5!|0lu=I;51T=Ws+`<1>hloP=J5{X=MuTU5>xTscXGgJd?S9Lh-9)s{-uSPtE#X=2$o9H_nnIEuX+a{73p(ldjfLK zg0*CUnXP<*??Z;MNgpX9*@3nELAGN(UrMUjx-YPx&j!Ac3}@~LA)Y<4fj7lTEDz#z zc=(`>o}kasBJwlw5FPh9H;L0N$?m~)xA8Gat|pWRXQM`KHJNp4gAO;|;V?VYO%}6G z*N6trY|yYtxAA#N8oQ;*Wigx87K6#GHZNJg7r?t_bu`t~nJ|jt|Gwbb6lv9nV3CGl;oLZbto!X&u z8Ljq4ld;iYWS%*EPLkEq=rS~F9cq`si~_20Sk(@T-k~;{99D}_tGBweX7=+OJ~hei z(mOR~g9C@tJJkl0%cQp2jX1jAqHA>MU0MUSESk$_C%KFkgVv(et2K7L28G^*2h5<+ zsLf3li(ThLGI93j=JLIgjCeen^msg+S_gUp29rZ=aXArlG#Lzfr(L6S8rX@sd{L4^ z+tjGTFSXNYN7bftp=#6WkZcW(fc}uP$z@`79sGbKJ!&9_-RMx8kXJaS8R^to%xbF+ z7jM!#T~4Q!J=wtZou-hHd$qa&*IE?lE zPCbU)pJAqG7l!9Ap%6a~?4?C~eiZN=#DE3L&{#eJO_*5WVFoOg{iH+G%dg}KiHCM6 z9q4Zkp$=kCNjA00$@!6=m7schc{-b|`} zQX1mryLi(#@^c6ad65^`9h>z9-jBxSWJw3+nIGZ;s$sgTQ2Zxffr8lRq?Qa!yrgf8i(K$%x7+pawn*_ zFJX3bubeYl3~a-GzSI}1a+t=vh5MeYqwUC~5>~sJPhv%0Tz7q^75>A^i!`T{t$UI0 zEio*GjPxC7W6lRn8wteFfdurO#&LFy#_nn~>C`%h8MT1RsZra_XupkmjoH}b((BA-JKNEw z$PSjMquf&*_Y_onx+W|Soj+ih5Mi~Fd)nkI3d`Q32xslv`Eb%G9jaJXu}2Zap2$PD zuHzu)x-QaqHX&Z<%}hJ^+xclzoRhuVckuD#!A*Q3pW^Oc#aj0$`mnS;iV)T%QV3*m zA1Z>0QxdyaHt$n~N~#+CC#${cf%gF5WGl{-N{&>LpXrlOjO)%Of$qM;g@`IEahI^i z_9=S!#;o{Ip^%ATT|ZPr5m%?_q$;-bQ$=#*#A)NlH%JsWv@|8AxKpczh#DNHs?#{; zPZUXHOsC#h(1k7fM4=6cA0b4PWAofj&A<3q5y)14tOzG9o%&#@nT3C`#dCOX(37^%eJ~C`Q9)PM3K6NP_@ zgYUu??^m?3;{A#+M)o4dChk?}NUOxLIQH^h#jRw54vnlCkl(z|Mp!yI)rpC%KK+`i?x8#^K)vk_C)at`*`b zjARe*Qy9raA5A~(Q)p2+CVZ(dlgYlK!CxwZ+3jB{Qpm0B*)J8n$P~|`A>rQrUn+W2 z+T6gtjTDkG`w|=_Wcrt}fP?k04;CP0w*gy_sci3@1Apa84*caHNlyh!R%pvrV|x%4 z>8Gf0-$5HBS)hl}0yQX0V&G*COJ7H8^Sd4T9w*rYQcn3XU@x~RGyTyV-U3;OlOCqgaXu7Y0<(!GDBKJG@BMJXi!Hy z5iRr~GuhkGLON+;iWs3UnZ;6Lgp5e15D^hU?$XM7rKQ#0?3BT5@4OgcDeWlGPJ`l z_{KE7&Z0M2g5w~Y+-Op+R0rL>I`HPz0XJ56NO9+t(N#CrcdizGuhy$|mW1m_S6)+X zzP?)Vht(ZdC+DxJH(lQyZeC4)kAac48+v!T#NB~=_4_4m{Qthi9R(fR&<1iZwjuDg zAXwjs;05XSb8Rhx=cKFLXKVp%D{%ZIh?SpSLkX ztY9I{Y|aO0nC7zW?<;z<^|3-c8mZnNC=y5qI~yyc3z02o6C+S$-&cr~nqf1;=9arzzIPd}$0 z(Rb+^^kur0Zlr7I({wdmftSS{OX$6HKJB10=@i;Zo2i}NLaS*xEvC6NgPN%p+0&QC z(pv5`WOjp=S9<~&ny-T3qLOz15q47|x-D!-x~BthcFc{ugQo;W;(H+0M4 zK@3?Iz@0D`gO|y83*>^)P>)yPWl#h;Fc?grfdP<&ryvr#K`ena=s zJ@kG0Hhq=upquFnsA8U=tLVe@pLAgdy&Lb(Z=>yWA{|Sc=&iJt4x^>CfM(HjYM^O& zsh&uC(Qq1uH@ZCeja(pS$oJ$BIY9Q3kI8%FP4WuaMmCYPgz*J~PX4@bOMJ@@2 zO^IT5jDA7iLv}t-AEoyr|Hjji$hEKtK5!{0_&w=T9^)wDIpBtbKs*r|4$c91q{y~Hk=!35y zB5;P6DMJn_ST9jHS_x6U({+PELnF5lL!YN%HLSn~C`;g8G&~(J6YsKHae;QY1s^t) zLowt+2ADw$sn8eVAqpq>FZEp?#AXTy{l$Hqg_ZjxlX*Ed!d@Y+Rfw>m1}TjY43wKMMT3n(`#6jkuyAsuFo1HmOYTo3TNNSj?8QhSMdenE z7b0@8{`&|4mC+qn>$|el&3qVpJwoVNlOk^h#pJ*_H2hzo(O-k!&6~*dxp>z8g*+L9 z+GZGP8Vm9v4!Yx}U&6!j3O+a~$D^UeL(!e`m}%NiKcN3cy>btoO{FB}^Tf-S354Dx zf4}q;kB>pL9N6hj8bhXWA9LCz*|A>jeZof&tK@ofa0}f)yOW9B+xXG>=N-0LxgY$G z2r$+BFW_k-IZlQV;GV=?SmJqdaIJFCM|EG>l+3y+?6WmOQm^RkXZlD6Mc^7a7>!FP#OF2*NI)ZJL=qB68o~jdKM@&KL5N*I=%kcR~*94oFZd6u4Z)p=lI5f1QxYgV-d|)zzyhK&B0^jZ&K!0i? zuu6?;u=oD=h1C!*H4f%h;qxQz2tJ4Sqo!_e!e_#0i1e+_!9fGQF)AaM$!c7kgEQdN z@&47d8dvbf?`(~pE~e%DfbTwu@jP#;%OL&7roZ_i7KcFgJ@@fOu2UYnLd?A! z+)YoS*3LsE3*5b&lzuAm6LUd3l@}r(nD3dWTfryqt^B~#Rll64*{N$h>-4p}%D!c_ zC&W;~$7cSr%QMpOK!_=R&VB9SDtXr%JfZ1oPpWZ9WLVVecgmguhuNVK^l4mpEuzv) zrGCEGt`MH0Y{FXQGk=Mnf=|ct%)Lv<@`mmcqCnOk3eL?z6%NwnUJm|Z|H-<+gG;X`Xk59#3oI?ZJ_n3o@cE(mcB(uQV!fZ z{b$<%k1Acq9^4{C@QDF)gW0+*NN>-@lM37Wdro1^+^s^EpxIpd4|iO$r)7|lUEC@- zh4j0+g}P(=c>)LP*|KedOR4C#^~d_Yo~H-vn0dR9lrjWHej(Ay8( ztVRD4X^X{YNI2lgHE8c=v{OveGK*QE2wb~ z(lZz`N1>G}g|TSK)&SXqWIe^+dP5oA!9!?(sDMx+DrAU>4#d03&@97j85YQ}M26)u ztdij<88*nUNro;Nj*;P58Dgps$DAO;i87ol!>J1Bz>0SHV}^h?H=MUX`TlAi?C%#nFOEE|fK0X?(5*9%c*pT$tNfymFTR5qp;0uNM5{am79wsmi|I0bS3ivw zaS+@6wtsstX6(UdNZW&01R28SzO9V&+rXPycyPz+JY7wCwsI3Hseuc!7S?M$YfU(CJRy|w(qTq@glD|@ZRifZ9^IXJkE2HF#9a9qw2Q3p&bo7~f>dkD;S@e6dHsFfV z*2KJMGkHP{I<|ZRu1Ji@72M%QPlG|vPHYewcq+c0#VR%mQw2rs#DVTOy=S6P&wkq| zG{+M4zuuD+sggtYTq0aSnz)znL;86?eM30>7ge8xU z)@N67#|r7aOY@Gpe)lT;Fg+CmFBr)^JF_}{S|lAdUWfT^|`Vg{q$EMiI4d22T|QaoEBj4&AW3`fkt`p*_zoL=AgaQR+RjP2m@7Y@S#1tcEu(DYOTsgLHxyGn zwKwrC&E8YTgr}}-$j$wTa`_m2^%DwRnCHV%TbX=}(k~##dz*A|?r@uYjPAXFLT7&W zojCTx1z{>5+7`TvxqcQNRi=Ix?T#HPADfJexN!65pMG3lEITJ(TojU$`c2C?{FfG) z6sXFDUQ*LQd{~6h(Lnkdbx|OXlWgu=?hdX51x{oogUJYEOXZlcQ>(_-SjUd5uG9=O zPMx#jJ7#+F&btJ0>`H95^iMV6sCEjqRbT?S)$dCjcqdY9d% zw~Nh1Q>WHU%AZ)^EUs=Zscg~a78Z@yX$z}!D;$Oi)w9M_PN{9x*G`(o^ciHJalF3O zXsR7nIxbspb5-X}t7@Jxqo%ypGP%9jZmF%SpJ|vQ%20vm`sMTIp)0P?v9di`XWc$_|h>gxti8m+bq{Kws{CiHsZRARX?VBhJKt$EG}y-Yi}N2TElu|5@XgM zKeN61^7DT_{ABlJl3vEibwjHR+LET~na%mbZBv?@RgU%%Y3(gDE7MBNGg@c1j%k@z zRh+A7EGwxQS75Jbon$GhVtX@5Ki}OT43TbXhpg|KpaXW#Ad;B%cV9QN-!~gf8 z?e*_$r8A)9;w7E1k#@xp?Iry9{pZb9s=mH*xNl}OS;$#bshY*~CHCA|RH@9(gagd-tx&*roD_1{c**5Yu&5X-H>--6*P>d?1hXpJAFE+B zI1*PKL~gw<8az!^!>%m)X112KokEKOv)lKwu+zAFUH?gqZ0%{GmQP+(6~Thfpb8$W zdu0{7*r;^L=GQql##DU;&tKU>NwRX zvT?U6k;Y%$vXK~7M_}mk*0o#qO7!nrc5?&SBwLxC165f<*n0^p-Eobwm9eT-%>wjB1TZ1vp<-|Q zTeRXZxs63H4wu^?Vljx-avL9RF^VJP>92lTOkxeoddu$;fAA){#C4KOj93J|S~>Zq zGLF6Xrn2`y*%`Nrfr0q{=ZIDjL$RRW(Jmnn?=RJA#bD;%t?UH`F_abWR>s?8s;qkP zRv%&%N6X|{jiS{g+B@M6nN(l7)gm_fBsopW4{f1QWGeR&m$oE3+IuQo_2JDur}5}H zXxZoeRgpr**8hxfhmS+gLB|H9s7CXd=br7y7N)4C@(Is9xriwTs8$H!^Z$9?9XkeH z2OZltK-H`a)`m^5Z${Ta%Vwpj`YJ<0#yD**^vASpPb!WY@l4rycevA2i|LHyfvVAw zR&KWJqAT$N`+9ogx+A?YlAHz-4fh90I4|bwdJ^`TSyj%aOX5vHmE~PISoI=dM~td0 ze^hca?}^#^s%n)(287M9O-8kAzuj(5aF*)VzpKZyO zGF1WbE)7%NNB=?y#n$##E$91`kGz{zB&urp#Q8;cvS$-fM_Bia64<3g)DfBG;vMbn zs1CL4jXtVuJ~a5ThgfiuY6VXRb|kUql2pqhwR8JF@@Ets8H!BrDKTBZ7qh=7N2v-V zcK=A&Lq=5vTQx*g$VBA1_wW!^EKw3@!2LrzTn#L=w`xUV>Mp&Ul?~!79Nuitf+V^f zug(Orf!MjT+(OQRBAGHAUkea?V#{T7%>jXb@a*`?WiqpuDvLv}yCY_pw(id~HYLx# zD^(7-p&I){GmA}`cS8+!Oko>r%9uZ@$-SXa9%yKpGWCWUnSy7_l 0 - def test_requestrepobuild_with_credentials(self): + def test_requestrepobuild_with_robot(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')) assert len(json['builds']) == 0 # Request a (fake) build. - pull_creds = { - 'username': 'foo', - 'password': 'bar', - 'registry': 'baz' - } + pull_robot = ADMIN_ACCESS_USER + '+dtrobot' self.postResponse(RepositoryBuildList, params=dict(repository=ADMIN_ACCESS_USER + '/simple'), - data=dict(file_id='foobarbaz', pull_credentials=pull_creds), + data=dict(file_id='foobarbaz', pull_robot=pull_robot), expected_code=201) # Check for the build. @@ -1007,7 +1003,28 @@ class TestRequestRepoBuild(ApiTestCase): 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): @@ -1746,7 +1763,7 @@ class TestBuildTriggers(ApiTestCase): # Verify that the robot was saved. self.assertEquals(True, activate_json['is_active']) - self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', activate_json['pull_user']['name']) + self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', activate_json['pull_robot']['name']) # Start a manual build. start_json = self.postJsonResponse(ActivateBuildTrigger, diff --git a/util/names.py b/util/names.py index b705bec36..57fafdd10 100644 --- a/util/names.py +++ b/util/names.py @@ -26,4 +26,7 @@ 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 6e8be1f4d..9d552a4ae 100644 --- a/workers/dockerfilebuild.py +++ b/workers/dockerfilebuild.py @@ -352,13 +352,14 @@ 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) tag_names = job_config['docker_tags'] build_subdir = job_config['build_subdir'] repo = job_config['repository'] - pull_credentials = job_config.get('pull_credentials', None) access_token = repository_build.access_token.code From 6ff46cc450e6566757ab5173636313620c2f3b34 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Apr 2014 22:12:51 -0400 Subject: [PATCH 08/58] Clarify upload language for .zip and .tar.gz --- static/directives/dockerfile-build-form.html | 2 +- static/partials/new-repo.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/directives/dockerfile-build-form.html b/static/directives/dockerfile-build-form.html index da808ec04..54f409b0a 100644 --- a/static/directives/dockerfile-build-form.html +++ b/static/directives/dockerfile-build-form.html @@ -12,7 +12,7 @@

- Upload a Dockerfile or a zip file containing a Dockerfile in the root directory + Upload a Dockerfile or an archive (.zip or .tar.gz) containing a Dockerfile in the root directory
diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index 686073a6b..87832987c 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -118,11 +118,11 @@
- +
- +
@@ -140,7 +140,7 @@
-
Upload DockerfileZIP file
+
Upload DockerfileArchive
Date: Wed, 2 Apr 2014 12:12:10 -0400 Subject: [PATCH 09/58] Update the docker-py dependency to point to our patched library. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0b942eee1..46def6790 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ beautifulsoup4==4.3.2 blinker==1.3 boto==2.27.0 distribute==0.6.34 -docker-py==0.3.0 +git+https://github.com/DevTable/docker-py.git ecdsa==0.11 gevent==1.0 greenlet==0.4.2 From 204fecc1f9d11c32eceb32c018203bc16fd056df Mon Sep 17 00:00:00 2001 From: jakedt Date: Wed, 2 Apr 2014 12:22:32 -0400 Subject: [PATCH 10/58] Restore the cache buster. --- endpoints/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/common.py b/endpoints/common.py index e25d7c797..749cf3ba0 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -91,7 +91,7 @@ def random_string(): def render_page_template(name, **kwargs): resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), - cache_buster='foobarbaz', **kwargs)) + cache_buster=random_string(), **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp From b95d3ec329f29efb6a1a188d3cc6d4a2d59eba85 Mon Sep 17 00:00:00 2001 From: jakedt Date: Wed, 2 Apr 2014 19:32:41 -0400 Subject: [PATCH 11/58] Add a watchdog timer to the build worker to kill a build step that takes more than 20 minutes. --- workers/dockerfilebuild.py | 31 +++++++++++++++++++++++++++++-- workers/worker.py | 12 +++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/workers/dockerfilebuild.py b/workers/dockerfilebuild.py index 9d552a4ae..744057574 100644 --- a/workers/dockerfilebuild.py +++ b/workers/dockerfilebuild.py @@ -12,6 +12,8 @@ from docker import Client, APIError from tempfile import TemporaryFile, mkdtemp from zipfile import ZipFile from functools import partial +from datetime import datetime, timedelta +from threading import Event from data.queue import dockerfile_build_queue from data import model @@ -31,6 +33,8 @@ logger = logging.getLogger(__name__) user_files = app.config['USERFILES'] build_logs = app.config['BUILDLOGS'] +TIMEOUT_PERIOD_MINUTES = 20 + class StatusWrapper(object): def __init__(self, build_uuid): @@ -316,6 +320,8 @@ class DockerfileBuildWorker(Worker): 'application/gzip': DockerfileBuildWorker.__prepare_tarball, } + self._timeout = Event() + @staticmethod def __prepare_zip(request_file): build_dir = mkdtemp(prefix='docker-build-') @@ -347,7 +353,24 @@ class DockerfileBuildWorker(Worker): return build_dir + def watchdog(self): + logger.debug('Running build watchdog code.') + + docker_cl = Client() + + # Iterate the running containers and kill ones that have been running more than 20 minutes + for container in docker_cl.containers(): + start_time = datetime.fromtimestamp(container[u'Created']) + running_time = datetime.now() - start_time + if running_time > timedelta(minutes=TIMEOUT_PERIOD_MINUTES): + logger.warning('Container has been running too long: %s with command: %s', + container[u'Id'], container[u'Command']) + docker_cl.kill(container[u'Id']) + self._timeout.set() + def process_queue_item(self, job_details): + self._timeout.clear() + repository_build = model.get_repository_build(job_details['namespace'], job_details['repository'], job_details['build_uuid']) @@ -370,7 +393,7 @@ class DockerfileBuildWorker(Worker): start_msg = ('Starting job with resource url: %s repo: %s' % (resource_url, repo)) - log_appender(start_msg) + logger.debug(start_msg) docker_resource = requests.get(resource_url, stream=True) c_type = docker_resource.headers['content-type'] @@ -402,7 +425,11 @@ class DockerfileBuildWorker(Worker): log_appender('error', build_logs.PHASE) repository_build.phase = 'error' repository_build.save() - log_appender('Unable to build dockerfile.', build_logs.ERROR) + if self._timeout.is_set(): + log_appender('Build step was terminated after %s minutes.' % TIMEOUT_PERIOD_MINUTES, + build_logs.ERROR) + else: + log_appender('Unable to build dockerfile.', build_logs.ERROR) return True log_appender('pushing', build_logs.PHASE) diff --git a/workers/worker.py b/workers/worker.py index be4984bdd..a525913a1 100644 --- a/workers/worker.py +++ b/workers/worker.py @@ -9,10 +9,12 @@ logger = logging.getLogger(__name__) class Worker(object): - def __init__(self, queue, poll_period_seconds=30, reservation_seconds=300): + def __init__(self, queue, poll_period_seconds=30, reservation_seconds=300, + watchdog_period_seconds=60): self._sched = Scheduler() self._poll_period_seconds = poll_period_seconds self._reservation_seconds = reservation_seconds + self._watchdog_period_seconds = watchdog_period_seconds self._stop = Event() self._queue = queue @@ -20,6 +22,10 @@ class Worker(object): """ Return True if complete, False if it should be retried. """ raise NotImplementedError('Workers must implement run.') + def watchdog(self): + """ Function that gets run once every watchdog_period_seconds. """ + pass + def poll_queue(self): logger.debug('Getting work item from queue.') @@ -43,8 +49,8 @@ class Worker(object): logger.debug("Scheduling worker.") self._sched.start() - self._sched.add_interval_job(self.poll_queue, - seconds=self._poll_period_seconds) + self._sched.add_interval_job(self.poll_queue, seconds=self._poll_period_seconds) + self._sched.add_interval_job(self.watchdog, seconds=self._watchdog_period_seconds) while not self._stop.wait(1): pass From 7c466dab7d82e10056af51889eb37f1427faa596 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 2 Apr 2014 23:33:58 -0400 Subject: [PATCH 12/58] - Add an analyze method on triggers that, when given trigger config, will attempt to analyze the trigger's Dockerfile and determine what pull credentials, if any, are needed and available - Move the build trigger setup UI into its own directive (makes things cleaner) - Fix a bug in the entitySearch directive around setting the current entity - Change the build trigger setup UI to use the new analyze method and flow better --- endpoints/api/trigger.py | 138 +++++++++++++++- endpoints/trigger.py | 50 +++++- initdb.py | 2 +- static/css/quay.css | 10 +- static/directives/entity-search.html | 2 +- static/directives/setup-trigger-dialog.html | 100 ++++++++++++ static/js/app.js | 171 +++++++++++++++++++- static/js/controllers.js | 60 +------ static/partials/repo-admin.html | 69 +------- test/data/test.db | Bin 544768 -> 544768 bytes test/test_api_security.py | 129 ++++++++++++++- test/test_api_usage.py | 87 +++++++++- util/dockerfileparse.py | 72 +++++++++ 13 files changed, 759 insertions(+), 131 deletions(-) create mode 100644 static/directives/setup-trigger-dialog.html create mode 100644 util/dockerfileparse.py diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index c62367f52..7798280a1 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -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//trigger//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//trigger//start') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ diff --git a/endpoints/trigger.py b/endpoints/trigger.py index d5573a513..57e32df66 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -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 diff --git a/initdb.py b/initdb.py index 854f83f8a..3ddb601f3 100644 --- a/initdb.py +++ b/initdb.py @@ -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'), diff --git a/static/css/quay.css b/static/css/quay.css index 919c0ddc9..93f32f0a1 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3456,7 +3456,7 @@ pre.command:before { position: relative; - height: 100px; + height: 75px; opacity: 1; } @@ -3607,10 +3607,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 { diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html index 04dac6c0f..bc1c0a94e 100644 --- a/static/directives/entity-search.html +++ b/static/directives/entity-search.html @@ -1,5 +1,5 @@ - + + + +
+
+
+
diff --git a/static/js/app.js b/static/js/app.js index 311b531fb..2aaf412c0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2650,6 +2650,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; } @@ -2732,7 +2733,9 @@ quayApp.directive('entitySearch', function () { }; $scope.clearEntityInternal = function() { + $scope.currentEntityInternal = null; $scope.currentEntity = null; + if ($scope.entitySelected) { $scope.entitySelected(null); } @@ -2746,6 +2749,7 @@ quayApp.directive('entitySearch', function () { } if ($scope.isPersistent) { + $scope.currentEntityInternal = entity; $scope.currentEntity = entity; } @@ -2870,6 +2874,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; @@ -3407,6 +3421,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, @@ -3416,15 +3569,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) { @@ -3435,6 +3591,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) { @@ -3473,6 +3630,7 @@ quayApp.directive('triggerSetupGithub', function () { $scope.locations = null; $scope.trigger.$ready = false; $scope.isInvalidLocation = false; + $scope.analyze({'isValid': false}); return; } @@ -3485,12 +3643,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}); }); } }; @@ -3535,7 +3695,14 @@ quayApp.directive('triggerSetupGithub', function () { }); }; - loadSources(); + var check = function() { + if ($scope.repository && $scope.trigger) { + loadSources(); + } + }; + + $scope.$watch('repository', check); + $scope.$watch('trigger', check); $scope.$watch('currentRepo', function(repo) { $scope.selectRepoInternal(repo); diff --git a/static/js/controllers.js b/static/js/controllers.js index 08bcca561..304347667 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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) { diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index f1ee224f5..e7656aaf9 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -377,76 +377,17 @@
-
{{ shownToken.friendlyName }}
- - - + +
-
+
-
+
An e-mail has been sent to {{ sentEmail }} to verify the change.
@@ -177,12 +186,12 @@
-
+
-
+
diff --git a/templates/base.html b/templates/base.html index 59e427da1..5a1638574 100644 --- a/templates/base.html +++ b/templates/base.html @@ -73,6 +73,7 @@ From 19a20a6c94efb9b659ffa11f17ce5ffcc380ba00 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sun, 6 Apr 2014 00:36:19 -0400 Subject: [PATCH 21/58] Turn off all references and API calls to billing if the feature is disabled --- endpoints/api/__init__.py | 6 ++ endpoints/api/billing.py | 12 +++- endpoints/api/organization.py | 5 +- endpoints/api/subscribe.py | 4 ++ endpoints/api/user.py | 11 ++-- endpoints/web.py | 4 ++ static/directives/header-bar.html | 2 +- static/js/app.js | 26 ++++++-- static/js/controllers.js | 91 ++++++++++++++++----------- static/partials/new-organization.html | 7 ++- static/partials/org-admin.html | 24 ++++--- static/partials/user-admin.html | 5 +- 12 files changed, 135 insertions(+), 62 deletions(-) diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 61c1f3e6a..97f766da7 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -124,6 +124,9 @@ def format_date(date): def add_method_metadata(name, value): def modifier(func): + if func is None: + return None + if '__api_metadata' not in dir(func): func.__api_metadata = {} func.__api_metadata[name] = value @@ -132,6 +135,9 @@ def add_method_metadata(name, value): def method_metadata(func, name): + if func is None: + return None + if '__api_metadata' in dir(func): return func.__api_metadata.get(name, None) return None diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 1f31aa58b..89dda31f0 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -4,13 +4,14 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin) + require_user_admin, show_if, hide_if) from endpoints.api.subscribe import subscribe, subscription_view from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from data import model from data.plans import PLANS +import features def carderror_response(e): return {'carderror': e.message}, 402 @@ -79,6 +80,7 @@ def get_invoices(customer_id): @resource('/v1/plans/') +@show_if(features.BILLING) class ListPlans(ApiResource): """ Resource for listing the available plans. """ @nickname('listPlans') @@ -91,6 +93,7 @@ class ListPlans(ApiResource): @resource('/v1/user/card') @internal_only +@show_if(features.BILLING) class UserCard(ApiResource): """ Resource for managing a user's credit card. """ schemas = { @@ -132,6 +135,7 @@ class UserCard(ApiResource): @resource('/v1/organization//card') @internal_only @related_user_resource(UserCard) +@show_if(features.BILLING) class OrganizationCard(ApiResource): """ Resource for managing an organization's credit card. """ schemas = { @@ -178,6 +182,7 @@ class OrganizationCard(ApiResource): @resource('/v1/user/plan') @internal_only +@show_if(features.BILLING) class UserPlan(ApiResource): """ Resource for managing a user's subscription. """ schemas = { @@ -234,6 +239,7 @@ class UserPlan(ApiResource): @resource('/v1/organization//plan') @internal_only @related_user_resource(UserPlan) +@show_if(features.BILLING) class OrganizationPlan(ApiResource): """ Resource for managing a org's subscription. """ schemas = { @@ -294,6 +300,7 @@ class OrganizationPlan(ApiResource): @resource('/v1/user/invoices') @internal_only +@show_if(features.BILLING) class UserInvoiceList(ApiResource): """ Resource for listing a user's invoices. """ @require_user_admin @@ -310,6 +317,7 @@ class UserInvoiceList(ApiResource): @resource('/v1/organization//invoices') @internal_only @related_user_resource(UserInvoiceList) +@show_if(features.BILLING) class OrgnaizationInvoiceList(ApiResource): """ Resource for listing an orgnaization's invoices. """ @nickname('listOrgInvoices') @@ -323,4 +331,4 @@ class OrgnaizationInvoiceList(ApiResource): return get_invoices(organization.stripe_id) - raise Unauthorized() \ No newline at end of file + raise Unauthorized() diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 9cb6a267a..f89ddc5d5 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -5,7 +5,7 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin, log_action) + require_user_admin, log_action, show_if) from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, @@ -15,6 +15,8 @@ from data import model from data.plans import get_plan from util.gravatar import compute_hash +import features + logger = logging.getLogger(__name__) @@ -163,6 +165,7 @@ class Organization(ApiResource): @resource('/v1/organization//private') @internal_only @related_user_resource(PrivateRepositories) +@show_if(features.BILLING) class OrgPrivateRepositories(ApiResource): """ Custom verb to compute whether additional private repositories are available. """ @nickname('getOrganizationPrivateAllowed') diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py index f9f9d7f14..efc2dfea7 100644 --- a/endpoints/api/subscribe.py +++ b/endpoints/api/subscribe.py @@ -6,6 +6,7 @@ from endpoints.common import check_repository_usage from data import model from data.plans import PLANS +import features logger = logging.getLogger(__name__) @@ -24,6 +25,9 @@ def subscription_view(stripe_subscription, used_repos): def subscribe(user, plan, token, require_business_plan): + if not features.BILLING: + return + plan_found = None for plan_obj in PLANS: if plan_obj['stripeId'] == plan: diff --git a/endpoints/api/user.py b/endpoints/api/user.py index f89d7ed62..40194a436 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -194,6 +194,7 @@ class User(ApiResource): @resource('/v1/user/private') @internal_only +@show_if(features.BILLING) class PrivateRepositories(ApiResource): """ Operations dealing with the available count of private repositories. """ @require_user_admin @@ -249,8 +250,7 @@ class ConvertToOrganization(ApiResource): 'description': 'Information required to convert a user to an organization.', 'required': [ 'adminUser', - 'adminPassword', - 'plan', + 'adminPassword' ], 'properties': { 'adminUser': { @@ -263,7 +263,7 @@ class ConvertToOrganization(ApiResource): }, 'plan': { 'type': 'string', - 'description': 'The plan to which the organizatino should be subscribed', + 'description': 'The plan to which the organization should be subscribed', }, }, }, @@ -290,8 +290,9 @@ class ConvertToOrganization(ApiResource): message='The admin user credentials are not valid') # Subscribe the organization to the new plan. - plan = convert_data['plan'] - subscribe(user, plan, None, True) # Require business plans + if features.BILLING: + plan = convert_data.get('plan', 'free') + subscribe(user, plan, None, True) # Require business plans # Convert the user to an organization. model.convert_user_to_organization(user, model.get_user(admin_username)) diff --git a/endpoints/web.py b/endpoints/web.py index 1e78b12af..e14c70e79 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -20,6 +20,8 @@ from util.names import parse_repository_name from util.gravatar import compute_hash from auth import scopes +import features + logger = logging.getLogger(__name__) web = Blueprint('web', __name__) @@ -54,6 +56,7 @@ def snapshot(path = ''): @web.route('/plans/') @no_cache +@route_show_if(features.BILLING) def plans(): return index('') @@ -152,6 +155,7 @@ def privacy(): @web.route('/receipt', methods=['GET']) +@route_show_if(features.BILLING) def receipt(): if not current_user.is_authenticated(): abort(401) diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 1a6ade0b7..05f7e24cf 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -14,7 +14,7 @@
  • Repositories
  • Docs
  • Tutorial
  • -
  • Pricing
  • +
  • Pricing
  • Organizations
  • diff --git a/static/js/app.js b/static/js/app.js index 532512bb0..4c60e1c4d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -876,8 +876,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return keyService; }]); - $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', - function(KeyService, UserService, CookieService, ApiService) { + $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', + function(KeyService, UserService, CookieService, ApiService, Features) { var plans = null; var planDict = {}; var planService = {}; @@ -903,7 +903,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.notePlan = function(planId) { - CookieService.putSession('quay.notedplan', planId); + if (Features.BILLING) { + CookieService.putSession('quay.notedplan', planId); + } }; planService.isOrgCompatible = function(plan) { @@ -929,7 +931,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu planService.handleNotedPlan = function() { var planId = planService.getAndResetNotedPlan(); - if (!planId) { return false; } + if (!planId || !Features.BILLING) { return false; } UserService.load(function() { if (UserService.currentUser().anonymous) { @@ -974,6 +976,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.verifyLoaded = function(callback) { + if (!Features.BILLING) { return; } + if (plans) { callback(plans); return; @@ -1033,10 +1037,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.getSubscription = function(orgname, success, failure) { - ApiService.getSubscription(orgname).then(success, failure); + if (!Features.BILLING) { return; } + + ApiService.getSubscription(orgname).then(success, failure); }; planService.setSubscription = function(orgname, planId, success, failure, opt_token) { + if (!Features.BILLING) { return; } + var subscriptionDetails = { plan: planId }; @@ -1056,6 +1064,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.getCardInfo = function(orgname, callback) { + if (!Features.BILLING) { return; } + ApiService.getCard(orgname).then(function(resp) { callback(resp.card); }, function() { @@ -1064,6 +1074,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.changePlan = function($scope, orgname, planId, callbacks) { + if (!Features.BILLING) { return; } + if (callbacks['started']) { callbacks['started'](); } @@ -1089,6 +1101,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.changeCreditCard = function($scope, orgname, callbacks) { + if (!Features.BILLING) { return; } + if (callbacks['opening']) { callbacks['opening'](); } @@ -1145,6 +1159,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) { + if (!Features.BILLING) { return; } + if (callbacks['opening']) { callbacks['opening'](); } diff --git a/static/js/controllers.js b/static/js/controllers.js index 08bcca561..33b17772f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1607,7 +1607,9 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams } function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService, - $routeParams, $http) { + $routeParams, $http, Features) { + $scope.Features = Features; + if ($routeParams['migrate']) { $('#migrateTab').tab('show') } @@ -1690,13 +1692,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use }; $scope.showConvertForm = function() { - PlanService.getMatchingBusinessPlan(function(plan) { - $scope.org.plan = plan; - }); + if (Features.BILLING) { + PlanService.getMatchingBusinessPlan(function(plan) { + $scope.org.plan = plan; + }); - PlanService.getPlans(function(plans) { - $scope.orgPlans = plans; - }); + PlanService.getPlans(function(plans) { + $scope.orgPlans = plans; + }); + } $scope.convertStep = 1; }; @@ -1711,7 +1715,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use var data = { 'adminUser': $scope.org.adminUser, 'adminPassword': $scope.org.adminPassword, - 'plan': $scope.org.plan.stripeId + 'plan': $scope.org.plan ? $scope.org.plan.stripeId : '' }; ApiService.convertUserToOrganization(data).then(function(resp) { @@ -1912,7 +1916,7 @@ function V1Ctrl($scope, $location, UserService) { UserService.updateUserIn($scope); } -function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) { +function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) { UserService.updateUserIn($scope); $scope.githubRedirectUri = KeyService.githubRedirectUri; @@ -2034,13 +2038,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService var checkPrivateAllowed = function() { if (!$scope.repo || !$scope.repo.namespace) { return; } + if (!Features.BILLING) { + $scope.checkingPlan = false; + $scope.planRequired = null; + return; + } + $scope.checkingPlan = true; var isUserNamespace = $scope.isUserNamespace; ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) { $scope.checkingPlan = false; - if (resp['privateAllowed']) { + if (resp['privateAllowed']) { $scope.planRequired = null; return; } @@ -2160,18 +2170,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) { loadOrganization(); } -function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) { +function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features) { var orgname = $routeParams.orgname; // Load the list of plans. - PlanService.getPlans(function(plans) { - $scope.plans = plans; - $scope.plan_map = {}; - - for (var i = 0; i < plans.length; ++i) { - $scope.plan_map[plans[i].stripeId] = plans[i]; - } - }); + if (Features.BILLING) { + PlanService.getPlans(function(plans) { + $scope.plans = plans; + $scope.plan_map = {}; + + for (var i = 0; i < plans.length; ++i) { + $scope.plan_map[plans[i].stripeId] = plans[i]; + } + }); + } $scope.orgname = orgname; $scope.membersLoading = true; @@ -2354,30 +2366,39 @@ function OrgsCtrl($scope, UserService) { browserchrome.update(); } -function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) { +function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) { + $scope.Features = Features; + $scope.holder = {}; + UserService.updateUserIn($scope); var requested = $routeParams['plan']; - // Load the list of plans. - PlanService.getPlans(function(plans) { - $scope.plans = plans; - $scope.currentPlan = null; - if (requested) { - PlanService.getPlan(requested, function(plan) { - $scope.currentPlan = plan; - }); - } - }); + if (Features.BILLING) { + // Load the list of plans. + PlanService.getPlans(function(plans) { + $scope.plans = plans; + $scope.currentPlan = null; + if (requested) { + PlanService.getPlan(requested, function(plan) { + $scope.currentPlan = plan; + }); + } + }); + } $scope.signedIn = function() { - PlanService.handleNotedPlan(); + if (Features.BILLING) { + PlanService.handleNotedPlan(); + } }; $scope.signinStarted = function() { - PlanService.getMinimumPlan(1, true, function(plan) { - PlanService.notePlan(plan.stripeId); - }); + if (Features.BILLING) { + PlanService.getMinimumPlan(1, true, function(plan) { + PlanService.notePlan(plan.stripeId); + }); + } }; $scope.setPlan = function(plan) { @@ -2409,7 +2430,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan }; // If the selected plan is free, simply move to the org page. - if ($scope.currentPlan.price == 0) { + if (!Features.BILLING || $scope.currentPlan.price == 0) { showOrg(); return; } diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index be99bae98..5f4756cfe 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -66,13 +66,14 @@
    -
    +
    Choose your organization's plan -
    +
    - diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 139579aa8..2b7b0be80 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -6,15 +6,23 @@ @@ -22,12 +30,12 @@
    -
    +
    -
    +
    @@ -67,12 +75,12 @@
    -
    +
    -
    +
    diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index fcfb157c1..0a0bb1f82 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -243,13 +243,14 @@
    -
    +
    - From badf002e92e5ad5283574ebd2a1ea001f0be615b Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sun, 6 Apr 2014 00:50:30 -0400 Subject: [PATCH 22/58] Turn off all references and API calls to github login if the feature is disabled --- endpoints/callbacks.py | 5 ++++- static/directives/signin-form.html | 5 +++-- static/directives/signup-form.html | 13 +++++++++---- static/js/app.js | 4 +++- static/js/controllers.js | 2 ++ static/partials/user-admin.html | 2 +- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 0f110c098..2f801af88 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -3,7 +3,7 @@ import logging from flask import request, redirect, url_for, Blueprint from flask.ext.login import current_user -from endpoints.common import render_page_template, common_login +from endpoints.common import render_page_template, common_login, route_show_if from app import app, mixpanel from data import model from util.names import parse_repository_name @@ -11,6 +11,7 @@ from util.http import abort from auth.permissions import AdministerRepositoryPermission from auth.auth import require_session_login +import features logger = logging.getLogger(__name__) @@ -48,6 +49,7 @@ def get_github_user(token): @callback.route('/github/callback', methods=['GET']) +@route_show_if(features.GITHUB_LOGIN) def github_oauth_callback(): error = request.args.get('error', None) if error: @@ -101,6 +103,7 @@ def github_oauth_callback(): @callback.route('/github/callback/attach', methods=['GET']) +@route_show_if(features.GITHUB_LOGIN) @require_session_login def github_oauth_attach(): token = exchange_github_code_for_token(request.args.get('code')) diff --git a/static/directives/signin-form.html b/static/directives/signin-form.html index 814955ce6..f56b8f8db 100644 --- a/static/directives/signin-form.html +++ b/static/directives/signin-form.html @@ -6,12 +6,13 @@ placeholder="Password" ng-model="user.password"> -