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) 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') @resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>/logs')
@path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('build_uuid', 'The UUID of the build') @path_param('build_uuid', 'The UUID of the build')
@ -368,33 +391,12 @@ class RepositoryBuildLogs(RepositoryParamResource):
@nickname('getRepoBuildLogs') @nickname('getRepoBuildLogs')
def get(self, namespace, repository, build_uuid): def get(self, namespace, repository, build_uuid):
""" Return the build logs for the build specified by the build uuid. """ """ Return the build logs for the build specified by the build uuid. """
response_obj = {}
build = model.build.get_repository_build(build_uuid) build = model.build.get_repository_build(build_uuid)
if (not build or build.repository.name != repository or if (not build or build.repository.name != repository or
build.repository.namespace_user.username != namespace): build.repository.namespace_user.username != namespace):
raise NotFound() raise NotFound()
# If the logs have been archived, just return a URL of the completed archive return get_logs_or_log_url(build)
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
@resource('/v1/filedrop/') @resource('/v1/filedrop/')

View file

@ -14,17 +14,20 @@ from flask import request, make_response, jsonify
import features import features
from app import (app, avatar, superusers, authentication, config_provider, license_validator, 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 import scopes
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth.permissions import SuperUserPermission from auth.permissions import SuperUserPermission
from data.buildlogs import BuildStatusRetrievalError
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
internal_only, require_scope, show_if, parse_args, internal_only, require_scope, show_if, parse_args,
query_param, abort, require_fresh_login, path_param, verify_not_prod, query_param, abort, require_fresh_login, path_param, verify_not_prod,
page_support, log_action, InvalidRequest) 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 endpoints.api.logs import get_logs, get_aggregate_logs
from data import model from data import model
from data.database import ServiceKeyApprovalType from data.database import ServiceKeyApprovalType
from endpoints.exception import NotFound
from util.useremails import send_confirmation_email, send_recovery_email from util.useremails import send_confirmation_email, send_recovery_email
from util.license import decode_license, LicenseDecodeError from util.license import decode_license, LicenseDecodeError
from util.security.ssl import load_certificate, CertInvalidException from util.security.ssl import load_certificate, CertInvalidException
@ -980,3 +983,61 @@ class SuperUserLicense(ApiResource):
} }
abort(403) 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; font-size: 16px !important;
border: none !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: { scope: {
'build': '=build', 'build': '=build',
'useTimestamps': '=useTimestamps', 'useTimestamps': '=useTimestamps',
'buildUpdated': '&buildUpdated' 'buildUpdated': '&buildUpdated',
'isSuperUser': '=isSuperUser'
}, },
controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray, controller: function($scope, $element, $interval, $sanitize, ansi2html, AngularViewArray,
AngularPollChannel, ApiService, Restangular, UtilService) { 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(); var result = $element.find('#copyButton').clipboardCopy();
if (!result) { if (!result) {
$element.find('#copyButton').hide(); $element.find('#copyButton').hide();
@ -95,7 +102,8 @@ angular.module('quay').directive('buildLogsView', function () {
'build_uuid': build.id '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; } if (resp.id != $scope.build.id) { callback(false); return; }
// Call the build updated handler. // Call the build updated handler.
@ -109,7 +117,7 @@ angular.module('quay').directive('buildLogsView', function () {
'start': $scope.logStartIndex '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 // If we get a logs url back, then we need to make another XHR request to retrieve the
// data. // data.
var logsUrl = resp['logs_url']; 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.currentConfig = null;
$scope.serviceKeysActive = false; $scope.serviceKeysActive = false;
$scope.globalMessagesActive = false; $scope.globalMessagesActive = false;
$scope.superUserBuildLogsActive = false;
$scope.manageUsersActive = false; $scope.manageUsersActive = false;
$scope.orderedOrgs = []; $scope.orderedOrgs = [];
$scope.orgsPerPage = 10; $scope.orgsPerPage = 10;
@ -43,6 +44,11 @@
$scope.loadMessageOfTheDay = function () { $scope.loadMessageOfTheDay = function () {
$scope.globalMessagesActive = true; $scope.globalMessagesActive = true;
}; };
$scope.loadSuperUserBuildLogs = function () {
$scope.superUserBuildLogsActive = true;
};
$scope.configurationSaved = function(config) { $scope.configurationSaved = function(config) {
$scope.currentConfig = config; $scope.currentConfig = config;
$scope.requiresRestart = true; $scope.requiresRestart = true;

View file

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

View file

@ -54,6 +54,10 @@
tab-init="loadMessageOfTheDay()"> tab-init="loadMessageOfTheDay()">
<i class="fa fa-newspaper-o"></i> <i class="fa fa-newspaper-o"></i>
</span> </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> <!-- /cor-tabs -->
<div class="cor-tab-content"> <div class="cor-tab-content">
@ -66,6 +70,11 @@
configuration-saved="configurationSaved(config)"></div> configuration-saved="configurationSaved(config)"></div>
</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 --> <!-- Messages tab -->
<div id="message-of-the-day" class="tab-pane"> <div id="message-of-the-day" class="tab-pane">
<div class="global-message-tab" is-enabled="globalMessagesActive"></div> <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, SuperUserServiceKey, SuperUserServiceKeyApproval,
SuperUserTakeOwnership, SuperUserLicense, SuperUserTakeOwnership, SuperUserLicense,
SuperUserCustomCertificates, SuperUserCustomCertificates,
SuperUserCustomCertificate) SuperUserCustomCertificate, SuperUserRepositoryBuildLogs,
SuperUserRepositoryBuildResource, SuperUserRepositoryBuildStatus)
from endpoints.api.globalmessages import GlobalUserMessage, GlobalUserMessages from endpoints.api.globalmessages import GlobalUserMessage, GlobalUserMessages
from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.secscan import RepositoryImageSecurity
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
@ -4332,6 +4333,60 @@ class TestSuperUserMessage(ApiTestCase):
self._run_test('DELETE', 204, 'devtable', None) 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): class TestUserInvoiceFieldList(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)