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
|
### v2.3.4
|
||||||
|
|
||||||
- Added: Always show tag expiration options in superuser panel
|
- Added: Always show tag expiration options in superuser panel
|
||||||
|
|
|
@ -58,6 +58,11 @@ class GlobalUserMessages(ApiResource):
|
||||||
'message': {
|
'message': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'A single message',
|
'description': 'A single message',
|
||||||
|
'required': [
|
||||||
|
'content',
|
||||||
|
'media_type',
|
||||||
|
'severity',
|
||||||
|
],
|
||||||
'properties': {
|
'properties': {
|
||||||
'content': {
|
'content': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
ng-if="entity"></div>
|
ng-if="entity"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" ng-show="view == 'enterName'">
|
<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>
|
<label>Provide a name for your new {{ entityTitle }}:</label>
|
||||||
<input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
|
<input type="text" class="form-control" ng-model="entityName" ng-pattern="entityNameRegexObj" required>
|
||||||
<div class="help-text">
|
<div class="help-text">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<td class="message-content">
|
<td class="message-content">
|
||||||
<span ng-switch on="message.media_type">
|
<span ng-switch on="message.media_type">
|
||||||
<span ng-switch-when="text/markdown">
|
<span ng-switch-when="text/markdown">
|
||||||
<span class="markdown-view" content="message.content"></span>
|
<markdown-view content="message.content"></markdown-view>
|
||||||
</span>
|
</span>
|
||||||
<span ng-switch-default>{{ message.content }}</span>
|
<span ng-switch-default>{{ message.content }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -81,7 +81,10 @@
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label>Message</label>
|
<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>
|
</div>
|
||||||
<div class="modal-footer" ng-show="createdMessage">
|
<div class="modal-footer" ng-show="createdMessage">
|
||||||
|
|
|
@ -76,7 +76,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
||||||
<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">
|
<span class="co-help-text">
|
||||||
Optional notes for additional human-readable information about why the key was created.
|
Optional notes for additional human-readable information about why the key was created.
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -251,7 +251,10 @@
|
||||||
<li ng-repeat="key in approveKeysInfo.keys">{{ getKeyTitle(key) }}</li>
|
<li ng-repeat="key in approveKeysInfo.keys">{{ getKeyTitle(key) }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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">
|
<span class="co-help-text">
|
||||||
Enter optional notes for additional human-readable information about why the keys were approved.
|
Enter optional notes for additional human-readable information about why the keys were approved.
|
||||||
</span>
|
</span>
|
||||||
|
@ -268,7 +271,10 @@
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 10px;">
|
||||||
Approve service key <strong>{{ getKeyTitle(approvalKeyInfo.key) }}</strong>?
|
Approve service key <strong>{{ getKeyTitle(approvalKeyInfo.key) }}</strong>?
|
||||||
</div>
|
</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">
|
<span class="co-help-text">
|
||||||
Enter optional notes for additional human-readable information about why the key was approved.
|
Enter optional notes for additional human-readable information about why the key was approved.
|
||||||
</span>
|
</span>
|
||||||
|
@ -344,7 +350,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><label for="create-key-notes">Approval Notes:</label></td>
|
<td><label for="create-key-notes">Approval Notes:</label></td>
|
||||||
<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">
|
<span class="co-help-text">
|
||||||
Optional notes for additional human-readable information about why the key was added.
|
Optional notes for additional human-readable information about why the key was added.
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -38,15 +38,15 @@ angular.module('quay').directive('globalMessageTab', function () {
|
||||||
|
|
||||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||||
$scope.creatingMessage = false;
|
$scope.creatingMessage = false;
|
||||||
$scope.newMessage = {
|
|
||||||
'media_type': 'text/markdown',
|
|
||||||
'severity': 'info'
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#createMessageModal').modal('hide');
|
$('#createMessageModal').modal('hide');
|
||||||
$scope.loadMessageInternal();
|
$scope.loadMessageInternal();
|
||||||
}, errorHandler)
|
}, errorHandler)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.updateMessage = function(content) {
|
||||||
|
$scope.newMessage.content = content;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showDeleteMessage = function (uuid) {
|
$scope.showDeleteMessage = function (uuid) {
|
||||||
$scope.messageToDelete = uuid;
|
$scope.messageToDelete = uuid;
|
||||||
|
|
|
@ -108,6 +108,10 @@ angular.module('quay').directive('requestServiceKeyDialog', function () {
|
||||||
$scope.keyCreated({'key': resp});
|
$scope.keyCreated({'key': resp});
|
||||||
}, ApiService.errorDisplay('Could not create service key'));
|
}, ApiService.errorDisplay('Could not create service key'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.updateNotes = function(content) {
|
||||||
|
$scope.preshared.notes = content;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.$watch('requestKeyInfo', function(info) {
|
$scope.$watch('requestKeyInfo', function(info) {
|
||||||
if (info && info.service) {
|
if (info && info.service) {
|
||||||
|
|
|
@ -31,8 +31,11 @@
|
||||||
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||||
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||||
<div class="result-description" ng-if="result.description">
|
<div class="result-description" ng-if="result.description">
|
||||||
<div class="description markdown-view" content="result.description"
|
<div class="description">
|
||||||
first-line-only="true" placeholder-needed="false"></div>
|
<markdown-view content="result.description"
|
||||||
|
first-line-only="true"
|
||||||
|
placeholder-needed="false"></markdown-view>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span ng-switch-when="application">
|
<span ng-switch-when="application">
|
||||||
|
|
|
@ -168,6 +168,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
||||||
loadServiceKeys();
|
loadServiceKeys();
|
||||||
}, ApiService.errorDisplay('Could not create service key'));
|
}, ApiService.errorDisplay('Could not create service key'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.updateNewKeyNotes = function(content) {
|
||||||
|
$scope.newKey.notes = content;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showApproveKey = function(key) {
|
$scope.showApproveKey = function(key) {
|
||||||
$scope.approvalKeyInfo = {
|
$scope.approvalKeyInfo = {
|
||||||
|
@ -195,6 +199,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
||||||
callback(true);
|
callback(true);
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.updateApprovalKeyInfoNotes = function(content) {
|
||||||
|
$scope.approvalKeyInfo.notes = content;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showCreateKey = function() {
|
$scope.showCreateKey = function() {
|
||||||
$scope.newKey = {
|
$scope.newKey = {
|
||||||
|
@ -351,7 +359,11 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
||||||
|
|
||||||
forAllKeys(info.keys, 'Could not approve service key', performer, callback);
|
forAllKeys(info.keys, 'Could not approve service key', performer, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.updateApproveKeysInfoNotes = function(content) {
|
||||||
|
$scope.approveKeysInfo.notes = content;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.changeKeysExpiration = function(info, callback) {
|
$scope.changeKeysExpiration = function(info, callback) {
|
||||||
var performer = function(key) {
|
var performer = function(key) {
|
||||||
var data = {
|
var data = {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as URI from 'urijs';
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
/**
|
/**
|
||||||
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
|
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
|
||||||
|
@ -236,11 +238,10 @@
|
||||||
$scope.serializeDbUri = function(fields) {
|
$scope.serializeDbUri = function(fields) {
|
||||||
if (!fields['server']) { return ''; }
|
if (!fields['server']) { return ''; }
|
||||||
|
|
||||||
|
var uri = URI();
|
||||||
try {
|
try {
|
||||||
if (!fields['server']) { return ''; }
|
if (!fields['server']) { return ''; }
|
||||||
if (!fields['database']) { return ''; }
|
if (!fields['database']) { return ''; }
|
||||||
|
|
||||||
var uri = URI();
|
|
||||||
uri = uri && uri.host(fields['server']);
|
uri = uri && uri.host(fields['server']);
|
||||||
uri = uri && uri.protocol(fields['kind']);
|
uri = uri && uri.protocol(fields['kind']);
|
||||||
uri = uri && uri.username(fields['username']);
|
uri = uri && uri.username(fields['username']);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<span class="cor-title-content">Quay Enterprise Setup</span>
|
<span class="cor-title-content">Quay Enterprise Setup</span>
|
||||||
</div>
|
</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">
|
<div class="co-alert alert alert-info">
|
||||||
<span class="cor-step-bar" progress="stepProgress">
|
<span class="cor-step-bar" progress="stepProgress">
|
||||||
<span class="cor-step" title="Upload License" text="1"></span>
|
<span class="cor-step" title="Upload License" text="1"></span>
|
||||||
|
|
|
@ -4318,7 +4318,7 @@ class TestSuperUserMessages(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(GlobalUserMessages)
|
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):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, None)
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
|
@ -57,16 +57,22 @@ class KubernetesConfigProvider(FileConfigProvider):
|
||||||
|
|
||||||
def volume_file_exists(self, filename):
|
def volume_file_exists(self, filename):
|
||||||
secret = self._lookup_secret()
|
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):
|
def list_volume_directory(self, path):
|
||||||
secret = self._lookup_secret()
|
secret = self._lookup_secret()
|
||||||
|
|
||||||
|
if not secret:
|
||||||
|
return []
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
for filename in secret:
|
for filename in secret.get('data', {}):
|
||||||
if filename.startswith(path):
|
if filename.startswith(path):
|
||||||
paths.append(filename[len(path) + 1:])
|
paths.append(filename[len(path) + 1:])
|
||||||
|
return paths
|
||||||
|
|
||||||
def remove_volume_file(self, filename):
|
def remove_volume_file(self, filename):
|
||||||
super(KubernetesConfigProvider, self).remove_volume_file(filename)
|
super(KubernetesConfigProvider, self).remove_volume_file(filename)
|
||||||
|
@ -126,7 +132,6 @@ class KubernetesConfigProvider(FileConfigProvider):
|
||||||
response = self._execute_k8s_api('GET', secret_url)
|
response = self._execute_k8s_api('GET', secret_url)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return json.loads(response.text)
|
return json.loads(response.text)
|
||||||
|
|
||||||
def _execute_k8s_api(self, method, relative_url, data=None):
|
def _execute_k8s_api(self, method, relative_url, data=None):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from mock import Mock
|
||||||
|
|
||||||
from util.config.provider import KubernetesConfigProvider
|
from util.config.provider import KubernetesConfigProvider
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ class TestKubernetesConfigProvider(KubernetesConfigProvider):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.yaml_filename = 'yaml_filename'
|
self.yaml_filename = 'yaml_filename'
|
||||||
self._service_token = 'service_token'
|
self._service_token = 'service_token'
|
||||||
|
self._execute_k8s_api = Mock()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('directory,filename,expected', [
|
@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)
|
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