diff --git a/CHANGELOG.md b/CHANGELOG.md index fd41424aa..943dc5779 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/endpoints/api/globalmessages.py b/endpoints/api/globalmessages.py index b27683a17..d6b491d2f 100644 --- a/endpoints/api/globalmessages.py +++ b/endpoints/api/globalmessages.py @@ -58,6 +58,11 @@ class GlobalUserMessages(ApiResource): 'message': { 'type': 'object', 'description': 'A single message', + 'required': [ + 'content', + 'media_type', + 'severity', + ], 'properties': { 'content': { 'type': 'string', diff --git a/static/directives/create-entity-dialog.html b/static/directives/create-entity-dialog.html index a3ef2d966..5e8bec9bf 100644 --- a/static/directives/create-entity-dialog.html +++ b/static/directives/create-entity-dialog.html @@ -29,7 +29,7 @@ ng-if="entity"> -
+ Enter optional notes for additional human-readable information about why the keys were approved. @@ -268,7 +271,10 @@
Approve service key {{ getKeyTitle(approvalKeyInfo.key) }}?
-
+ Enter optional notes for additional human-readable information about why the key was approved. @@ -344,7 +350,10 @@ -
+ Optional notes for additional human-readable information about why the key was added. diff --git a/static/js/directives/ui/global-message-tab.js b/static/js/directives/ui/global-message-tab.js index e0534cd2f..8f4ac572b 100644 --- a/static/js/directives/ui/global-message-tab.js +++ b/static/js/directives/ui/global-message-tab.js @@ -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; diff --git a/static/js/directives/ui/request-service-key-dialog.js b/static/js/directives/ui/request-service-key-dialog.js index 9c0ed1320..bd3351567 100644 --- a/static/js/directives/ui/request-service-key-dialog.js +++ b/static/js/directives/ui/request-service-key-dialog.js @@ -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) { diff --git a/static/js/directives/ui/search-box/search-box.component.html b/static/js/directives/ui/search-box/search-box.component.html index 69495614a..48d756197 100644 --- a/static/js/directives/ui/search-box/search-box.component.html +++ b/static/js/directives/ui/search-box/search-box.component.html @@ -31,8 +31,11 @@ {{ result.namespace.name }}/{{ result.name }}
-
+
+ +
diff --git a/static/js/directives/ui/service-keys-manager.js b/static/js/directives/ui/service-keys-manager.js index 97a265194..d6a4da38f 100644 --- a/static/js/directives/ui/service-keys-manager.js +++ b/static/js/directives/ui/service-keys-manager.js @@ -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 = { diff --git a/static/js/pages/setup.js b/static/js/pages/setup.js index ab1938fe1..8ee89d590 100644 --- a/static/js/pages/setup.js +++ b/static/js/pages/setup.js @@ -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']); diff --git a/static/partials/setup.html b/static/partials/setup.html index a59c506e6..b7229e124 100644 --- a/static/partials/setup.html +++ b/static/partials/setup.html @@ -6,7 +6,7 @@ Quay Enterprise Setup -
+
diff --git a/test/test_api_security.py b/test/test_api_security.py index 0df692b17..40638dd24 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -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) diff --git a/util/config/provider/k8sprovider.py b/util/config/provider/k8sprovider.py index 7af71634d..c3965f015 100644 --- a/util/config/provider/k8sprovider.py +++ b/util/config/provider/k8sprovider.py @@ -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): diff --git a/util/config/provider/test/test_k8sprovider.py b/util/config/provider/test/test_k8sprovider.py index c31f69596..d64dcee7b 100644 --- a/util/config/provider/test/test_k8sprovider.py +++ b/util/config/provider/test/test_k8sprovider.py @@ -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") \ No newline at end of file