Merge pull request #2303 from charltonaustin/view_build_logs_as_superuser_137910387

feature(superuser panel): ability to view logs
This commit is contained in:
Charlton Austin 2017-01-27 12:32:31 -05:00 committed by GitHub
commit 2dfae9e892
10 changed files with 224 additions and 27 deletions

View file

@ -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/<apirepopath:repository>/build/<build_uuid>/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/')

View file

@ -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/<build_uuid>/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/<build_uuid>/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_uuid>/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)

View file

@ -3797,3 +3797,9 @@ i.mesos-icon {
font-size: 16px !important;
border: none !important;
}
.build-log-view {
float: left;
padding-right: .5em;
}

View file

@ -0,0 +1,18 @@
<!-- Messages tab -->
<div class="super-user-build-logs-element">
<div class="manager-header" header-title="Build Logs">
<span class="build-log-view">
<input class="form-control" type="text" ng-model="buildParams.buildUuid" placeholder="Build ID"
style="margin-right: 10px;">
</span>
<button class="create-button btn btn-primary" ng-click="loadBuild()">
<i class="fa fa-plus" style="margin-right: 6px;"></i>Get Logs
</button>
</div>
<div class="build-logs-view"
build="build"
use-timestamps="showLogTimestamps"
build-updated="setUpdatedBuild(build)"
is-super-user="true"
ng-show="build"></div>
</div>

View file

@ -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'];

View file

@ -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;
});

View file

@ -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;

View file

@ -58,6 +58,7 @@
build="originalBuild"
use-timestamps="showLogTimestamps"
build-updated="setUpdatedBuild(build)"
is-super-user="false"
ng-show="!originalBuild.error"></div>
</div>
</div>

View file

@ -54,6 +54,10 @@
tab-init="loadMessageOfTheDay()">
<i class="fa fa-newspaper-o"></i>
</span>
<span class="cor-tab hidden-xs" tab-title="Build Logs" tab-target="#super-user-build-logs"
tab-init="loadSuperUserBuildLogs()">
<i class="fa fa-history"></i>
</span>
</div> <!-- /cor-tabs -->
<div class="cor-tab-content">
@ -66,6 +70,11 @@
configuration-saved="configurationSaved(config)"></div>
</div>
<!-- Super user build logs tab-->
<div id="super-user-build-logs" class="tab-pane">
<div class="super-user-build-logs" is-enabled="superUserBuildLogsActive"></div>
</div> <!-- Super user build logs tab -->
<!-- Messages tab -->
<div id="message-of-the-day" class="tab-pane">
<div class="global-message-tab" is-enabled="globalMessagesActive"></div>

View file

@ -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)