Merge v2.4.0-release into cherrypick-2.4.0
This commit is contained in:
commit
939ddfd1d7
15 changed files with 120 additions and 21 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,3 +1,26 @@
|
|||
### v2.4.0
|
||||
|
||||
- Added: Kubernetes Applications Support
|
||||
- Added: Full-page search UI (#2529)
|
||||
- Added: Always generate V2 manifests for tag operations in UI (#2608)
|
||||
- Added: Option to enable public repositories in v2 catalog API (#2654)
|
||||
- Added: Disable repository notifications after 3 failures (#2652)
|
||||
- Added: Remove requirement for flash for copy button in UI (#2667)
|
||||
|
||||
- Fixed: Upgrade support for Markdown (#2624)
|
||||
- Fixed: Kubernetes secret generation with secrets with CAPITAL names (#2640)
|
||||
- Fixed: Content-Length reporting on HEAD requests (#2616)
|
||||
- Fixed: Use configured email address as the sender in email notifications (#2635)
|
||||
- Fixed: Better peformance on permissions lookup (#2628)
|
||||
- Fixed: Disable federated login for new users if user creation is disabled (#2623)
|
||||
- Fixed: Show build logs timestamps by default (#2647)
|
||||
- Fixed: Custom TLS certificates tooling in superuser panel under Kubernetes (#2646, #2663)
|
||||
- Fixed: Disable debug logs in superuser panel when under multiple instances (#2663)
|
||||
- Fixed: External Notification Modal UI bug (#2650)
|
||||
- Fixed: Security worker thrashing when security scanner not available
|
||||
- Fixed: Torrent validation in superuser config panel (#2694)
|
||||
- Fixed: Expensive database call in build badges (#2688)
|
||||
|
||||
### v2.3.4
|
||||
|
||||
- Added: Always show tag expiration options in superuser panel
|
||||
|
|
|
@ -58,6 +58,11 @@ class GlobalUserMessages(ApiResource):
|
|||
'message': {
|
||||
'type': 'object',
|
||||
'description': 'A single message',
|
||||
'required': [
|
||||
'content',
|
||||
'media_type',
|
||||
'severity',
|
||||
],
|
||||
'properties': {
|
||||
'content': {
|
||||
'type': 'string',
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
ng-if="entity"></div>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="view == 'enterName'">
|
||||
<form name="enterNameForm" ng-submit="createEntity()">
|
||||
<form name="enterNameForm" ng-submit="enterNameForm.$valid && createEntity()">
|
||||
<label>Provide a name for your new {{ entityTitle }}:</label>
|
||||
<input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
|
||||
<div class="help-text">
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<td class="message-content">
|
||||
<span ng-switch on="message.media_type">
|
||||
<span ng-switch-when="text/markdown">
|
||||
<span class="markdown-view" content="message.content"></span>
|
||||
<markdown-view content="message.content"></markdown-view>
|
||||
</span>
|
||||
<span ng-switch-default>{{ message.content }}</span>
|
||||
</span>
|
||||
|
@ -81,7 +81,10 @@
|
|||
</select>
|
||||
|
||||
<label>Message</label>
|
||||
<div class="markdown-editor" content="newMessage.content"></div>
|
||||
<markdown-input content="newMessage.content"
|
||||
can-write="true"
|
||||
(content-changed)="updateMessage($event.content)"
|
||||
field-title="message"></markdown-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="createdMessage">
|
||||
|
|
|
@ -76,7 +76,10 @@
|
|||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
||||
<td>
|
||||
<div class="markdown-editor" content="preshared.notes"></div>
|
||||
<markdown-input content="preshared.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was created.
|
||||
</span>
|
||||
|
|
|
@ -251,7 +251,10 @@
|
|||
<li ng-repeat="key in approveKeysInfo.keys">{{ getKeyTitle(key) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="markdown-editor" content="approveKeysInfo.notes"></div>
|
||||
<markdown-input content="approveKeysInfo.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateApproveKeysInfoNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Enter optional notes for additional human-readable information about why the keys were approved.
|
||||
</span>
|
||||
|
@ -268,7 +271,10 @@
|
|||
<div style="margin-bottom: 10px;">
|
||||
Approve service key <strong>{{ getKeyTitle(approvalKeyInfo.key) }}</strong>?
|
||||
</div>
|
||||
<div class="markdown-editor" content="approvalKeyInfo.notes"></div>
|
||||
<markdown-input content="approvalKeysInfo.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateApprovalKeyInfoNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Enter optional notes for additional human-readable information about why the key was approved.
|
||||
</span>
|
||||
|
@ -344,7 +350,10 @@
|
|||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes:</label></td>
|
||||
<td>
|
||||
<div class="markdown-editor" content="newKey.notes"></div>
|
||||
<markdown-input content="newKey.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateNewKeyNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was added.
|
||||
</span>
|
||||
|
|
|
@ -38,16 +38,16 @@ angular.module('quay').directive('globalMessageTab', function () {
|
|||
|
||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||
$scope.creatingMessage = false;
|
||||
$scope.newMessage = {
|
||||
'media_type': 'text/markdown',
|
||||
'severity': 'info'
|
||||
};
|
||||
|
||||
$('#createMessageModal').modal('hide');
|
||||
$scope.loadMessageInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.updateMessage = function(content) {
|
||||
$scope.newMessage.content = content;
|
||||
};
|
||||
|
||||
$scope.showDeleteMessage = function (uuid) {
|
||||
$scope.messageToDelete = uuid;
|
||||
$('#confirmDeleteMessageModal').modal({});
|
||||
|
|
|
@ -109,6 +109,10 @@ angular.module('quay').directive('requestServiceKeyDialog', function () {
|
|||
}, ApiService.errorDisplay('Could not create service key'));
|
||||
};
|
||||
|
||||
$scope.updateNotes = function(content) {
|
||||
$scope.preshared.notes = content;
|
||||
};
|
||||
|
||||
$scope.$watch('requestKeyInfo', function(info) {
|
||||
if (info && info.service) {
|
||||
$scope.show();
|
||||
|
|
|
@ -31,8 +31,11 @@
|
|||
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||
<div class="result-description" ng-if="result.description">
|
||||
<div class="description markdown-view" content="result.description"
|
||||
first-line-only="true" placeholder-needed="false"></div>
|
||||
<div class="description">
|
||||
<markdown-view content="result.description"
|
||||
first-line-only="true"
|
||||
placeholder-needed="false"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span ng-switch-when="application">
|
||||
|
|
|
@ -169,6 +169,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
|||
}, ApiService.errorDisplay('Could not create service key'));
|
||||
};
|
||||
|
||||
$scope.updateNewKeyNotes = function(content) {
|
||||
$scope.newKey.notes = content;
|
||||
};
|
||||
|
||||
$scope.showApproveKey = function(key) {
|
||||
$scope.approvalKeyInfo = {
|
||||
'key': key,
|
||||
|
@ -196,6 +200,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
|||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.updateApprovalKeyInfoNotes = function(content) {
|
||||
$scope.approvalKeyInfo.notes = content;
|
||||
};
|
||||
|
||||
$scope.showCreateKey = function() {
|
||||
$scope.newKey = {
|
||||
'expiration': null
|
||||
|
@ -352,6 +360,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
|||
forAllKeys(info.keys, 'Could not approve service key', performer, callback);
|
||||
};
|
||||
|
||||
$scope.updateApproveKeysInfoNotes = function(content) {
|
||||
$scope.approveKeysInfo.notes = content;
|
||||
};
|
||||
|
||||
$scope.changeKeysExpiration = function(info, callback) {
|
||||
var performer = function(key) {
|
||||
var data = {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as URI from 'urijs';
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
|
||||
|
@ -236,11 +238,10 @@
|
|||
$scope.serializeDbUri = function(fields) {
|
||||
if (!fields['server']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
try {
|
||||
if (!fields['server']) { return ''; }
|
||||
if (!fields['database']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
uri = uri && uri.host(fields['server']);
|
||||
uri = uri && uri.protocol(fields['kind']);
|
||||
uri = uri && uri.username(fields['username']);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<span class="cor-title-content">Quay Enterprise Setup</span>
|
||||
</div>
|
||||
|
||||
<div class="cor-tab-panel" style="padding: 20px;">
|
||||
<div class="co-main-content-panel" style="padding: 20px;">
|
||||
<div class="co-alert alert alert-info">
|
||||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<span class="cor-step" title="Upload License" text="1"></span>
|
||||
|
|
|
@ -4318,7 +4318,7 @@ class TestSuperUserMessages(ApiTestCase):
|
|||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(GlobalUserMessages)
|
||||
self.message = {'message': {'content': '', 'severity': 'info', 'media_type': 'text/plain'}}
|
||||
self.message = {'message': {'content': 'msg', 'severity': 'info', 'media_type': 'text/plain'}}
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
|
|
@ -57,16 +57,22 @@ class KubernetesConfigProvider(FileConfigProvider):
|
|||
|
||||
def volume_file_exists(self, filename):
|
||||
secret = self._lookup_secret()
|
||||
return filename in secret
|
||||
if not secret or not secret.get('data'):
|
||||
return False
|
||||
return filename in secret['data']
|
||||
|
||||
|
||||
def list_volume_directory(self, path):
|
||||
secret = self._lookup_secret()
|
||||
|
||||
if not secret:
|
||||
return []
|
||||
|
||||
paths = []
|
||||
for filename in secret:
|
||||
for filename in secret.get('data', {}):
|
||||
if filename.startswith(path):
|
||||
paths.append(filename[len(path) + 1:])
|
||||
return paths
|
||||
|
||||
def remove_volume_file(self, filename):
|
||||
super(KubernetesConfigProvider, self).remove_volume_file(filename)
|
||||
|
@ -126,7 +132,6 @@ class KubernetesConfigProvider(FileConfigProvider):
|
|||
response = self._execute_k8s_api('GET', secret_url)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
return json.loads(response.text)
|
||||
|
||||
def _execute_k8s_api(self, method, relative_url, data=None):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
from mock import Mock
|
||||
|
||||
from util.config.provider import KubernetesConfigProvider
|
||||
|
||||
|
@ -9,6 +10,7 @@ class TestKubernetesConfigProvider(KubernetesConfigProvider):
|
|||
def __init__(self):
|
||||
self.yaml_filename = 'yaml_filename'
|
||||
self._service_token = 'service_token'
|
||||
self._execute_k8s_api = Mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('directory,filename,expected', [
|
||||
|
@ -23,3 +25,32 @@ def test_get_volume_path(directory, filename, expected):
|
|||
assert expected == provider.get_volume_path(directory, filename)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('response,expected', [
|
||||
(Mock(text="{\"data\": {\"license\":\"test\"}}", status_code=200), {"data": {"license":"test"}}),
|
||||
(Mock(text="{\"data\": {\"license\":\"test\"}}", status_code=404), None),
|
||||
])
|
||||
def test_lookup_secret(response, expected):
|
||||
provider = TestKubernetesConfigProvider()
|
||||
provider._execute_k8s_api.return_value = response
|
||||
assert expected == provider._lookup_secret()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('response,key,expected', [
|
||||
(Mock(text="{\"data\": {\"license\":\"test\"}}", status_code=200), "license", True),
|
||||
(Mock(text="{\"data\": {\"license\":\"test\"}}", status_code=200), "config.yaml", False),
|
||||
(Mock(text="", status_code=404), "license", False),
|
||||
])
|
||||
def test_volume_file_exists(response, key, expected):
|
||||
provider = TestKubernetesConfigProvider()
|
||||
provider._execute_k8s_api.return_value = response
|
||||
assert expected == provider.volume_file_exists(key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('response,expected', [
|
||||
(Mock(text="{\"data\": {\"extra_license\":\"test\"}}", status_code=200), ["license"]),
|
||||
(Mock(text="", status_code=404), []),
|
||||
])
|
||||
def test_list_volume_directory(response, expected):
|
||||
provider = TestKubernetesConfigProvider()
|
||||
provider._execute_k8s_api.return_value = response
|
||||
assert expected == provider.list_volume_directory("extra")
|
Reference in a new issue