diff --git a/endpoints/api/build.py b/endpoints/api/build.py index b64f3ad37..ae97571eb 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -359,6 +359,29 @@ class RepositoryBuildStatus(RepositoryParamResource): return build_status_view(build) +def get_logs_or_log_url(build): + # If the logs have been archived, just return a URL of the completed archive + if build.logs_archived: + return { + 'logs_url': log_archive.get_file_url(build.uuid, requires_cors=True) + } + start = int(request.args.get('start', 0)) + + try: + count, logs = build_logs.get_log_entries(build.uuid, start) + except BuildStatusRetrievalError: + count, logs = (0, []) + + response_obj = {} + response_obj.update({ + 'start': start, + 'total': count, + 'logs': [log for log in logs], + }) + + return response_obj + + @resource('/v1/repository//build//logs') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('build_uuid', 'The UUID of the build') @@ -368,33 +391,12 @@ class RepositoryBuildLogs(RepositoryParamResource): @nickname('getRepoBuildLogs') def get(self, namespace, repository, build_uuid): """ Return the build logs for the build specified by the build uuid. """ - response_obj = {} - build = model.build.get_repository_build(build_uuid) if (not build or build.repository.name != repository or build.repository.namespace_user.username != namespace): raise NotFound() - # If the logs have been archived, just return a URL of the completed archive - if build.logs_archived: - return { - 'logs_url': log_archive.get_file_url(build.uuid, requires_cors=True) - } - - start = int(request.args.get('start', 0)) - - try: - count, logs = build_logs.get_log_entries(build.uuid, start) - except BuildStatusRetrievalError: - count, logs = (0, []) - - response_obj.update({ - 'start': start, - 'total': count, - 'logs': [log for log in logs], - }) - - return response_obj + return get_logs_or_log_url(build) @resource('/v1/filedrop/') diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 1474b84c8..9a1da3dea 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -14,17 +14,20 @@ from flask import request, make_response, jsonify import features from app import (app, avatar, superusers, authentication, config_provider, license_validator, - all_queues) + all_queues, log_archive, build_logs) from auth import scopes from auth.auth_context import get_authenticated_user from auth.permissions import SuperUserPermission +from data.buildlogs import BuildStatusRetrievalError from endpoints.api import (ApiResource, nickname, resource, validate_json_request, internal_only, require_scope, show_if, parse_args, query_param, abort, require_fresh_login, path_param, verify_not_prod, page_support, log_action, InvalidRequest) +from endpoints.api.build import build_status_view, get_logs_or_log_url from endpoints.api.logs import get_logs, get_aggregate_logs from data import model from data.database import ServiceKeyApprovalType +from endpoints.exception import NotFound from util.useremails import send_confirmation_email, send_recovery_email from util.license import decode_license, LicenseDecodeError from util.security.ssl import load_certificate, CertInvalidException @@ -980,3 +983,61 @@ class SuperUserLicense(ApiResource): } abort(403) + + +@resource('/v1/superuser//logs') +@path_param('build_uuid', 'The UUID of the build') +@show_if(features.SUPER_USERS) +class SuperUserRepositoryBuildLogs(ApiResource): + """ Resource for loading repository build logs for the superuser. """ + @require_fresh_login + @verify_not_prod + @nickname('getRepoBuildLogsSuperUser') + @require_scope(scopes.SUPERUSER) + def get(self, build_uuid): + """ Return the build logs for the build specified by the build uuid. """ + if not SuperUserPermission().can(): + abort(403) + + return get_logs_or_log_url(model.build.get_repository_build(build_uuid)) + + +@resource('/v1/superuser//status') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('build_uuid', 'The UUID of the build') +@show_if(features.SUPER_USERS) +class SuperUserRepositoryBuildStatus(ApiResource): + """ Resource for dealing with repository build status. """ + @require_fresh_login + @verify_not_prod + @nickname('getRepoBuildStatusSuperUser') + @require_scope(scopes.SUPERUSER) + def get(self, build_uuid): + """ Return the status for the builds specified by the build uuids. """ + if not SuperUserPermission().can(): + abort(403) + build = model.build.get_repository_build(build_uuid) + return build_status_view(build) + + +@resource('/v1/superuser//build') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('build_uuid', 'The UUID of the build') +@show_if(features.SUPER_USERS) +class SuperUserRepositoryBuildResource(ApiResource): + """ Resource for dealing with repository builds as a super user. """ + @require_fresh_login + @verify_not_prod + @nickname('getRepoBuildSuperUser') + @require_scope(scopes.SUPERUSER) + def get(self, build_uuid): + """ Returns information about a build. """ + if not SuperUserPermission().can(): + abort(403) + + try: + build = model.build.get_repository_build(build_uuid) + except model.build.InvalidRepositoryBuildException: + raise NotFound() + + return build_status_view(build) diff --git a/static/css/quay.css b/static/css/quay.css index 334d21011..e02a5beca 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3797,3 +3797,9 @@ i.mesos-icon { font-size: 16px !important; border: none !important; } + +.build-log-view { + float: left; + padding-right: .5em; +} + diff --git a/static/directives/super-user-build-logs.html b/static/directives/super-user-build-logs.html new file mode 100644 index 000000000..64a7415b5 --- /dev/null +++ b/static/directives/super-user-build-logs.html @@ -0,0 +1,18 @@ + +
+
+ + + + +
+
+
diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js index 9d1dd7569..e37da1dce 100644 --- a/static/js/directives/ui/build-logs-view.js +++ b/static/js/directives/ui/build-logs-view.js @@ -11,11 +11,18 @@ angular.module('quay').directive('buildLogsView', function () { scope: { 'build': '=build', 'useTimestamps': '=useTimestamps', - 'buildUpdated': '&buildUpdated' + 'buildUpdated': '&buildUpdated', + 'isSuperUser': '=isSuperUser' }, controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray, AngularPollChannel, ApiService, Restangular, UtilService) { + var repoStatusApiCall = ApiService.getRepoBuildStatus; + var repoLogApiCall = ApiService.getRepoBuildLogsAsResource; + if( $scope.isSuperUser ){ + repoStatusApiCall = ApiService.getRepoBuildStatusSuperUser; + repoLogApiCall = ApiService.getRepoBuildLogsSuperUserAsResource; + } var result = $element.find('#copyButton').clipboardCopy(); if (!result) { $element.find('#copyButton').hide(); @@ -95,7 +102,8 @@ angular.module('quay').directive('buildLogsView', function () { 'build_uuid': build.id }; - ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { + + repoStatusApiCall(null, params, true).then(function(resp) { if (resp.id != $scope.build.id) { callback(false); return; } // Call the build updated handler. @@ -109,7 +117,7 @@ angular.module('quay').directive('buildLogsView', function () { 'start': $scope.logStartIndex }; - ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { + repoLogApiCall(params, true).withOptions(options).get(function(resp) { // If we get a logs url back, then we need to make another XHR request to retrieve the // data. var logsUrl = resp['logs_url']; diff --git a/static/js/directives/ui/super-user-build-logs.js b/static/js/directives/ui/super-user-build-logs.js new file mode 100644 index 000000000..aa1e3b7f1 --- /dev/null +++ b/static/js/directives/ui/super-user-build-logs.js @@ -0,0 +1,31 @@ +/** + * An element for managing global messages. + */ +angular.module('quay').directive('superUserBuildLogs', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/super-user-build-logs.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'isEnabled': '=isEnabled' + }, + controller: function ($scope, $element, ApiService) { + $scope.buildParams = {}; + $scope.showLogTimestamps = true; + $scope.loadBuild = function () { + var params = { + 'build_uuid': $scope.buildParams.buildUuid + }; + ApiService.getRepoBuildSuperUserAsResource(params).get(function (build) { + $scope.build = build; + }); + }; + $scope.setUpdatedBuild = function (build) { + $scope.build = build; + }; + } + }; + return directiveDefinitionObject; +}); diff --git a/static/js/pages/superuser.js b/static/js/pages/superuser.js index 5557bca67..b94db2314 100644 --- a/static/js/pages/superuser.js +++ b/static/js/pages/superuser.js @@ -30,6 +30,7 @@ $scope.currentConfig = null; $scope.serviceKeysActive = false; $scope.globalMessagesActive = false; + $scope.superUserBuildLogsActive = false; $scope.manageUsersActive = false; $scope.orderedOrgs = []; $scope.orgsPerPage = 10; @@ -43,6 +44,11 @@ $scope.loadMessageOfTheDay = function () { $scope.globalMessagesActive = true; }; + + $scope.loadSuperUserBuildLogs = function () { + $scope.superUserBuildLogsActive = true; + }; + $scope.configurationSaved = function(config) { $scope.currentConfig = config; $scope.requiresRestart = true; diff --git a/static/partials/build-view.html b/static/partials/build-view.html index 3b379c691..1c6a86d20 100644 --- a/static/partials/build-view.html +++ b/static/partials/build-view.html @@ -58,6 +58,7 @@ build="originalBuild" use-timestamps="showLogTimestamps" build-updated="setUpdatedBuild(build)" + is-super-user="false" ng-show="!originalBuild.error"> diff --git a/static/partials/super-user.html b/static/partials/super-user.html index eddde9a74..ec6c6b519 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -54,6 +54,10 @@ tab-init="loadMessageOfTheDay()"> +
@@ -66,6 +70,11 @@ configuration-saved="configurationSaved(config)">
+ +
+
+
+
diff --git a/test/test_api_security.py b/test/test_api_security.py index a23ff75ed..f76812298 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -53,7 +53,8 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana SuperUserServiceKey, SuperUserServiceKeyApproval, SuperUserTakeOwnership, SuperUserLicense, SuperUserCustomCertificates, - SuperUserCustomCertificate) + SuperUserCustomCertificate, SuperUserRepositoryBuildLogs, + SuperUserRepositoryBuildResource, SuperUserRepositoryBuildStatus) from endpoints.api.globalmessages import GlobalUserMessage, GlobalUserMessages from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel @@ -4332,6 +4333,60 @@ class TestSuperUserMessage(ApiTestCase): self._run_test('DELETE', 204, 'devtable', None) +class TestSuperUserRepositoryBuildLogs(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserRepositoryBuildLogs, build_uuid='1234') + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 400, 'devtable', None) + + +class TestSuperUserRepositoryBuildStatus(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserRepositoryBuildStatus, build_uuid='1234') + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 400, 'devtable', None) + + +class TestSuperUserRepositoryBuildResource(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserRepositoryBuildResource, build_uuid='1234') + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 404, 'devtable', None) + + class TestUserInvoiceFieldList(ApiTestCase): def setUp(self): ApiTestCase.setUp(self)