Merge v2.4.0-release into cherrypick-2.4.0

This commit is contained in:
Evan Cordell 2017-07-10 10:25:18 -04:00
commit 939ddfd1d7
15 changed files with 120 additions and 21 deletions

View file

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

View file

@ -58,6 +58,11 @@ class GlobalUserMessages(ApiResource):
'message': {
'type': 'object',
'description': 'A single message',
'required': [
'content',
'media_type',
'severity',
],
'properties': {
'content': {
'type': 'string',

View file

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

View file

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

View file

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

View file

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

View file

@ -38,15 +38,15 @@ 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;

View file

@ -108,6 +108,10 @@ angular.module('quay').directive('requestServiceKeyDialog', function () {
$scope.keyCreated({'key': resp});
}, ApiService.errorDisplay('Could not create service key'));
};
$scope.updateNotes = function(content) {
$scope.preshared.notes = content;
};
$scope.$watch('requestKeyInfo', function(info) {
if (info && info.service) {

View file

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

View file

@ -168,6 +168,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
loadServiceKeys();
}, ApiService.errorDisplay('Could not create service key'));
};
$scope.updateNewKeyNotes = function(content) {
$scope.newKey.notes = content;
};
$scope.showApproveKey = function(key) {
$scope.approvalKeyInfo = {
@ -195,6 +199,10 @@ angular.module('quay').directive('serviceKeysManager', function () {
callback(true);
}, errorHandler);
};
$scope.updateApprovalKeyInfoNotes = function(content) {
$scope.approvalKeyInfo.notes = content;
};
$scope.showCreateKey = function() {
$scope.newKey = {
@ -351,7 +359,11 @@ 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 = {

View file

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

View file

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

View file

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

View file

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

View file

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