Merge pull request #2303 from charltonaustin/view_build_logs_as_superuser_137910387
feature(superuser panel): ability to view logs
This commit is contained in:
commit
2dfae9e892
10 changed files with 224 additions and 27 deletions
|
@ -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/')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
18
static/directives/super-user-build-logs.html
Normal file
18
static/directives/super-user-build-logs.html
Normal 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>
|
|
@ -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'];
|
||||||
|
|
31
static/js/directives/ui/super-user-build-logs.js
Normal file
31
static/js/directives/ui/super-user-build-logs.js
Normal 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;
|
||||||
|
});
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Reference in a new issue