def delete(self, namespace_name, repo_name, trigger_uuid): """ Delete the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if handler.is_active(): try: handler.deactivate() except TriggerException as ex: # We are just going to eat this error logger.warning("Trigger deactivation problem: %s", ex) log_action( "delete_repo_trigger", namespace_name, { "repo": repo_name, "trigger_id": trigger_uuid, "service": trigger.service.name }, repo=model.repository.get_repository(namespace_name, repo_name), ) trigger.delete_instance(recursive=True) if trigger.write_token is not None: trigger.write_token.delete_instance() return "No Content", 204
def attach_bitbucket_trigger(namespace_name, repo_name): permission = AdministerRepositoryPermission(namespace_name, repo_name) if permission.can(): repo = model.repository.get_repository(namespace_name, repo_name) if not repo: msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name) abort(404, message=msg) elif repo.kind.name != 'image': abort(501) trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None, current_user.db_user()) try: oauth_info = BuildTriggerHandler.get_handler(trigger).get_oauth_url() except TriggerProviderException: trigger.delete_instance() logger.debug('Could not retrieve Bitbucket OAuth URL') abort(500) config = { 'access_token': oauth_info['access_token'] } access_token_secret = oauth_info['access_token_secret'] model.build.update_build_trigger(trigger, config, auth_token=access_token_secret) return redirect(oauth_info['url']) abort(403)
def post(self, namespace_name, repo_name, trigger_uuid): """ Analyze the specified build trigger configuration. """ trigger = get_trigger(trigger_uuid) if trigger.repository.namespace_user.username != namespace_name: raise NotFound() if trigger.repository.name != repo_name: raise NotFound() new_config_dict = request.get_json()['config'] handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) server_hostname = app.config['SERVER_HOSTNAME'] try: trigger_analyzer = TriggerAnalyzer(handler, namespace_name, server_hostname, new_config_dict, AdministerOrganizationPermission(namespace_name).can()) return trigger_analyzer.analyze_trigger() except TriggerException as rre: return { 'status': 'error', 'message': 'Could not analyze the repository: %s' % rre.message, } except NotImplementedError: return { 'status': 'notimplemented', }
def trigger_view(trigger, can_read=False, can_admin=False, for_build=False): if trigger and trigger.uuid: build_trigger = BuildTriggerHandler.get_handler(trigger) build_source = build_trigger.config.get("build_source") repo_url = build_trigger.get_repository_url() if build_source else None can_read = can_read or can_admin trigger_data = { "id": trigger.uuid, "service": trigger.service.name, "is_active": build_trigger.is_active(), "build_source": build_source if can_read else None, "repository_url": repo_url if can_read else None, "config": build_trigger.config if can_admin else {}, "can_invoke": can_admin, "enabled": trigger.enabled, "disabled_reason": trigger.disabled_reason.name if trigger.disabled_reason else None, } if not for_build and can_admin and trigger.pull_robot: trigger_data["pull_robot"] = user_view(trigger.pull_robot) return trigger_data return None
def post(self, namespace_name, repo_name, trigger_uuid): """ List the subdirectories available for the specified build trigger and source. """ trigger = get_trigger(trigger_uuid) user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): new_config_dict = request.get_json() handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) try: subdirs = handler.list_build_subdirs() context_map = {} for file in subdirs: context_map = handler.get_parent_directory_mappings(file, context_map) return { 'dockerfile_paths': ['/' + subdir for subdir in subdirs], 'contextMap': context_map, 'status': 'success', } except EmptyRepositoryException as exc: return { 'status': 'success', 'contextMap': {}, 'dockerfile_paths': [], } except TriggerException as exc: return { 'status': 'error', 'message': exc.message, } else: raise Unauthorized()
def put(self, namespace_name, repo_name, trigger_uuid): """ Updates the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if not handler.is_active(): raise InvalidRequest("Cannot update an unactivated trigger") enable = request.get_json()["enabled"] model.build.toggle_build_trigger(trigger, enable) log_action( "toggle_repo_trigger", namespace_name, { "repo": repo_name, "trigger_id": trigger_uuid, "service": trigger.service.name, "enabled": enable, }, repo=model.repository.get_repository(namespace_name, repo_name), ) return trigger_view(trigger)
def trigger_view(trigger, can_read=False, can_admin=False, for_build=False): if trigger and trigger.uuid: build_trigger = BuildTriggerHandler.get_handler(trigger) build_source = build_trigger.config.get('build_source') repo_url = build_trigger.get_repository_url() if build_source else None can_read = can_read or can_admin trigger_data = { 'id': trigger.uuid, 'service': trigger.service.name, 'is_active': build_trigger.is_active(), 'build_source': build_source if can_read else None, 'repository_url': repo_url if can_read else None, 'config': build_trigger.config if can_admin else {}, 'can_invoke': can_admin, 'enabled': trigger.enabled, 'disabled_reason': trigger.disabled_reason.name if trigger.disabled_reason else None, } if not for_build and can_admin and trigger.pull_robot: trigger_data['pull_robot'] = user_view(trigger.pull_robot) return trigger_data return None
def post(self, namespace_name, repo_name, trigger_uuid): """ Manually start a build from the specified trigger. """ trigger = get_trigger(trigger_uuid) if not trigger.enabled: raise InvalidRequest('Trigger is not enabled.') handler = BuildTriggerHandler.get_handler(trigger) if not handler.is_active(): raise InvalidRequest('Trigger is not active.') try: repo = model.repository.get_repository(namespace_name, repo_name) pull_robot_name = model.build.get_pull_robot_name(trigger) run_parameters = request.get_json() prepared = handler.manual_start(run_parameters=run_parameters) build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name) except TriggerException as tse: raise InvalidRequest(tse.message) except MaximumBuildsQueuedException: abort(429, message='Maximum queued build rate exceeded.') except BuildTriggerDisabledException: abort(400, message='Build trigger is disabled') resp = build_status_view(build_request) repo_string = '%s/%s' % (namespace_name, repo_name) headers = { 'Location': api.url_for(RepositoryBuildStatus, repository=repo_string, build_uuid=build_request.uuid), } return resp, 201, headers
def get(self, namespace_name, repo_name, trigger_uuid): """ List the build sources for the trigger configuration thus far. """ trigger = get_trigger(trigger_uuid) user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): handler = BuildTriggerHandler.get_handler(trigger) try: return {"namespaces": handler.list_build_source_namespaces()} except TriggerException as rre: raise InvalidRequest(rre.message) else: raise Unauthorized()
def put(self, namespace_name, repo_name, trigger_uuid): """ Updates the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if not handler.is_active(): raise InvalidRequest('Cannot update an unactivated trigger') enable = request.get_json()['enabled'] model.build.toggle_build_trigger(trigger, enable) log_action('toggle_repo_trigger', namespace_name, {'repo': repo_name, 'trigger_id': trigger_uuid, 'service': trigger.service.name, 'enabled': enable}, repo=model.repository.get_repository(namespace_name, repo_name)) return trigger_view(trigger)
def post(self, namespace_name, repo_name, trigger_uuid, field_name): """ List the field values for a custom run field. """ trigger = get_trigger(trigger_uuid) config = request.get_json() or None if AdministerRepositoryPermission(namespace_name, repo_name).can(): handler = BuildTriggerHandler.get_handler(trigger, config) values = handler.list_field_values(field_name, limit=FIELD_VALUE_LIMIT) if values is None: raise NotFound() return {"values": values} else: raise Unauthorized()
def post(self, namespace_name, repo_name, trigger_uuid): """ List the build sources for the trigger configuration thus far. """ namespace = request.get_json().get("namespace") if namespace is None: raise InvalidRequest() trigger = get_trigger(trigger_uuid) user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): handler = BuildTriggerHandler.get_handler(trigger) try: return {"sources": handler.list_build_sources_for_namespace(namespace)} except TriggerException as rre: raise InvalidRequest(str(rre)) from rre else: raise Unauthorized()
def attach_bitbucket_build_trigger(trigger_uuid): trigger = model.build.get_build_trigger(trigger_uuid) if not trigger or trigger.service.name != BitbucketBuildTrigger.service_name(): abort(404) if trigger.connected_user != current_user.db_user(): abort(404) verifier = request.args.get("oauth_verifier") handler = BuildTriggerHandler.get_handler(trigger) result = handler.exchange_verifier(verifier) if not result: trigger.delete_instance() return "Token has expired" namespace = trigger.repository.namespace_user.username repository = trigger.repository.name repo_path = "%s/%s" % (namespace, repository) full_url = url_for("web.buildtrigger", path=repo_path, trigger=trigger.uuid) logger.debug("Redirecting to full url: %s", full_url) return redirect(full_url)
def to_dict(self): if not self.uuid: return None build_trigger = BuildTriggerHandler.get_handler(self) build_source = build_trigger.config.get("build_source") repo_url = build_trigger.get_repository_url() if build_source else None can_read = self.can_read or self.can_admin trigger_data = { "id": self.uuid, "service": self.service_name, "is_active": build_trigger.is_active(), "build_source": build_source if can_read else None, "repository_url": repo_url if can_read else None, "config": build_trigger.config if self.can_admin else {}, "can_invoke": self.can_admin, } if not self.for_build and self.can_admin and self.pull_robot: trigger_data["pull_robot"] = user_view(self.pull_robot) return trigger_data
def to_dict(self): if not self.uuid: return None build_trigger = BuildTriggerHandler.get_handler(self) build_source = build_trigger.config.get('build_source') repo_url = build_trigger.get_repository_url() if build_source else None can_read = self.can_read or self.can_admin trigger_data = { 'id': self.uuid, 'service': self.service_name, 'is_active': build_trigger.is_active(), 'build_source': build_source if can_read else None, 'repository_url': repo_url if can_read else None, 'config': build_trigger.config if self.can_admin else {}, 'can_invoke': self.can_admin, } if not self.for_build and self.can_admin and self.pull_robot: trigger_data['pull_robot'] = user_view(self.pull_robot) return trigger_data
def post(self, namespace_name, repo_name, trigger_uuid): """ Activate the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if handler.is_active(): raise InvalidRequest("Trigger config is not sufficient for activation.") user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): # Update the pull robot (if any). pull_robot_name = request.get_json().get("pull_robot", None) if pull_robot_name: try: pull_robot = model.user.lookup_robot(pull_robot_name) except model.InvalidRobotException: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, _) = 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_name: raise Unauthorized() # Set the pull robot. trigger.pull_robot = pull_robot # Update the config. new_config_dict = request.get_json()["config"] write_token_name = "Build Trigger: %s" % trigger.service.name write_token = model.token.create_delegate_token( namespace_name, repo_name, write_token_name, "write" ) try: path = url_for("webhooks.build_trigger_webhook", trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url( app.config["PREFERRED_URL_SCHEME"], "$token", write_token.get_code(), app.config["SERVER_HOSTNAME"], path, ) handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) final_config, private_config = handler.activate(authed_url) if "private_key" in private_config: trigger.secure_private_key = DecryptedValue(private_config["private_key"]) except TriggerException as exc: write_token.delete_instance() raise request_error(message=exc.message) # Save the updated config. update_build_trigger(trigger, final_config, write_token=write_token) # Log the trigger setup. repo = model.repository.get_repository(namespace_name, repo_name) log_action( "setup_repo_trigger", namespace_name, { "repo": repo_name, "namespace": namespace_name, "trigger_id": trigger.uuid, "service": trigger.service.name, "pull_robot": trigger.pull_robot.username if trigger.pull_robot else None, "config": final_config, }, repo=repo, ) return trigger_view(trigger, can_admin=True) else: raise Unauthorized()
def build_trigger_webhook(trigger_uuid, **kwargs): logger.debug("Webhook received with uuid %s", trigger_uuid) try: trigger = model.build.get_build_trigger(trigger_uuid) except model.InvalidBuildTriggerException: # It is ok to return 404 here, since letting an attacker know that a trigger UUID is valid # doesn't leak anything abort(404) # Ensure we are not currently in read-only mode. if app.config.get("REGISTRY_STATE", "normal") == "readonly": abort(503, "System is currently in read-only mode") # Ensure the trigger has permission. namespace = trigger.repository.namespace_user.username repository = trigger.repository.name if ModifyRepositoryPermission(namespace, repository).can(): handler = BuildTriggerHandler.get_handler(trigger) if trigger.repository.kind.name != "image": abort( 501, "Build triggers cannot be invoked on application repositories") if trigger.repository.state != RepositoryState.NORMAL: abort(503, "Repository is currently in read only or mirror mode") logger.debug("Passing webhook request to handler %s", handler) try: prepared = handler.handle_trigger_request(request) except ValidationRequestException: logger.debug("Handler reported a validation exception: %s", handler) # This was just a validation request, we don't need to build anything return make_response("Okay") except SkipRequestException: logger.debug("Handler reported to skip the build: %s", handler) # The build was requested to be skipped return make_response("Okay") except InvalidPayloadException as ipe: logger.exception("Invalid payload") # The payload was malformed abort(400, message=str(ipe)) pull_robot_name = model.build.get_pull_robot_name(trigger) repo = model.repository.get_repository(namespace, repository) try: start_build(repo, prepared, pull_robot_name=pull_robot_name) except MaximumBuildsQueuedException: abort(429, message="Maximum queued build rate exceeded.") except BuildTriggerDisabledException: logger.debug("Build trigger %s is disabled", trigger_uuid) abort( 400, message= "This build trigger is currently disabled. Please re-enable to continue.", ) return make_response("Okay") abort(403)
def post(self, namespace_name, repo_name, trigger_uuid): """ Activate the specified build trigger. """ trigger = get_trigger(trigger_uuid) handler = BuildTriggerHandler.get_handler(trigger) if handler.is_active(): raise InvalidRequest('Trigger config is not sufficient for activation.') user_permission = UserAdminPermission(trigger.connected_user.username) if user_permission.can(): # Update the pull robot (if any). pull_robot_name = request.get_json().get('pull_robot', None) if pull_robot_name: try: pull_robot = model.user.lookup_robot(pull_robot_name) except model.InvalidRobotException: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, _) = 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_name: raise Unauthorized() # Set the pull robot. trigger.pull_robot = pull_robot # Update the config. new_config_dict = request.get_json()['config'] write_token_name = 'Build Trigger: %s' % trigger.service.name write_token = model.token.create_delegate_token(namespace_name, repo_name, write_token_name, 'write') try: path = url_for('webhooks.build_trigger_webhook', trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', write_token.get_code(), app.config['SERVER_HOSTNAME'], path) handler = BuildTriggerHandler.get_handler(trigger, new_config_dict) final_config, private_config = handler.activate(authed_url) if 'private_key' in private_config: trigger.secure_private_key = DecryptedValue(private_config['private_key']) # TODO(remove-unenc): Remove legacy field. if ActiveDataMigration.has_flag(ERTMigrationFlags.WRITE_OLD_FIELDS): trigger.private_key = private_config['private_key'] except TriggerException as exc: write_token.delete_instance() raise request_error(message=exc.message) # Save the updated config. update_build_trigger(trigger, final_config, write_token=write_token) # Log the trigger setup. repo = model.repository.get_repository(namespace_name, repo_name) log_action('setup_repo_trigger', namespace_name, {'repo': repo_name, 'namespace': namespace_name, 'trigger_id': trigger.uuid, 'service': trigger.service.name, 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None, 'config': final_config}, repo=repo) return trigger_view(trigger, can_admin=True) else: raise Unauthorized()