From b0fdeed6366231990c8784cc68d70ddf6ba0b364 Mon Sep 17 00:00:00 2001 From: Simon Merrick Date: Wed, 18 Sep 2019 18:06:43 +1200 Subject: [PATCH] Add rolling upgrade ui + Create new row action on clusters panel + Create new modal form for upgrading cluster + Create REST endpoint for upgrading cluster + Bump python-magnumclient lower constraint (cherry picked from commit cd0817a13b58cbd7cb083e15d3467ce9f845ec32) Register the Cluster Upgrade view + Fixes missing url registraion for upgrade cluster in prior patch (https://review.opendev.org/#/c/697000/4). (cherry picked from commit d96f4d16c2b3254b290ff5d257340434a6dd6717) Change-Id: Id3fd3ee80fb27b08673933800aea6e7ee7ac7cd0 --- doc/source/configuration/index.rst | 32 ++- magnum_ui/api/magnum.py | 7 + magnum_ui/api/rest/magnum.py | 57 ++++- .../clusters/actions.module.js | 9 + .../rolling-upgrade/upgrade.service.js | 227 ++++++++++++++++++ .../rolling-upgrade/upgrade.service.spec.js | 139 +++++++++++ .../container-infra/magnum.service.js | 13 +- .../container-infra/magnum.service.spec.js | 26 ++ .../container-infra/utils.service.js | 75 ++++++ .../container-infra/utils.service.spec.js | 42 ++++ .../upgrade-actions-adf2f749ec0cc817.yaml | 4 + 11 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 magnum_ui/static/dashboard/container-infra/clusters/rolling-upgrade/upgrade.service.js create mode 100644 magnum_ui/static/dashboard/container-infra/clusters/rolling-upgrade/upgrade.service.spec.js create mode 100644 magnum_ui/static/dashboard/container-infra/utils.service.js create mode 100644 magnum_ui/static/dashboard/container-infra/utils.service.spec.js create mode 100644 releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst index f2b1b44b..67d551fe 100644 --- a/doc/source/configuration/index.rst +++ b/doc/source/configuration/index.rst @@ -2,7 +2,37 @@ Configuration ============= -Magnum UI has no configuration option. +Magnum UI specific settings +=========================== + +CLUSTER_TEMPLATE_GROUP_FILTERS +------------------------------ + +.. versionadded:: 5.3.0 (Ussuri) + +Default: ``None`` + +Examples: + +.. code-block:: python + + CLUSTER_TEMPLATE_GROUP_FILTERS = { + "dev": ".*-dev-.*", + "prod": ".*-prod-.*" + } + +The settings expects a dictionary of group name, to group regex. + +When set allows a cloud provider to specify template groups +for their cluster templates based on their naming convention. +This helps limit users from upgrading their cluster to an invalid +template that will not work based on their current template type. + +This filtering is only relevant when choosing a new template for +upgrading a cluster. + +Horizon Settings +================ For more configurations, see `Configuration Guide diff --git a/magnum_ui/api/magnum.py b/magnum_ui/api/magnum.py index 39e17684..aed85689 100644 --- a/magnum_ui/api/magnum.py +++ b/magnum_ui/api/magnum.py @@ -209,6 +209,13 @@ def cluster_resize(request, cluster_id, node_count, nodes_to_remove=nodes_to_remove, nodegroup=nodegroup) +def cluster_upgrade(request, cluster_uuid, cluster_template, + max_batch_size=1, nodegroup=None): + return magnumclient(request).clusters.upgrade( + cluster_uuid, cluster_template, + max_batch_size=max_batch_size, nodegroup=None) + + def certificate_create(request, **kwargs): args = {} for (key, value) in kwargs.items(): diff --git a/magnum_ui/api/rest/magnum.py b/magnum_ui/api/rest/magnum.py index 37ef251d..2f86909b 100644 --- a/magnum_ui/api/rest/magnum.py +++ b/magnum_ui/api/rest/magnum.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import defaultdict + +from django.conf import settings from django.http import HttpResponseNotFound from django.views import generic @@ -23,6 +26,8 @@ from openstack_dashboard.api import neutron from openstack_dashboard.api.rest import urls from openstack_dashboard.api.rest import utils as rest_utils +import re + def change_to_id(obj): """Change key named 'uuid' to 'id' @@ -70,9 +75,40 @@ class ClusterTemplates(generic.View): The returned result is an object with property 'items' and each item under this is a Cluster Template. + + If a GET query param for 'related_to' is specified, and + the setting for template filtering is set, then Horizon will + only return template groups which the given template + falls into, or all if none match. """ - result = magnum.cluster_template_list(request) - return {'items': [change_to_id(n.to_dict()) for n in result]} + templates = magnum.cluster_template_list(request) + + template_filters = getattr( + settings, "CLUSTER_TEMPLATE_GROUP_FILTERS", None) + related_to_id = request.GET.get("related_to") + + if template_filters and related_to_id: + templates_by_id = {t.uuid: t for t in templates} + related_to = templates_by_id.get(related_to_id) + + if related_to: + matched_groups = [] + groups = defaultdict(list) + for group, regex in template_filters.items(): + pattern = re.compile(regex) + if pattern.match(related_to.name): + matched_groups.append(group) + for template in templates: + if pattern.match(template.name): + groups[group].append(template) + + if matched_groups: + new_templates = [] + for group in matched_groups: + new_templates += groups[group] + templates = set(new_templates) + + return {'items': [change_to_id(n.to_dict()) for n in templates]} @rest_utils.ajax(data_required=True) def delete(self, request): @@ -164,6 +200,23 @@ class ClusterResize(generic.View): return HttpResponseNotFound() +@urls.register +class ClusterUpgrade(generic.View): + + url_regex = r'container_infra/clusters/(?P[^/]+)/upgrade$' + + @rest_utils.ajax(data_required=True) + def post(self, request, cluster_id): + """Upgrade a cluster""" + cluster_template = request.DATA.get("cluster_template") + max_batch_size = request.DATA.get("max_batch_size", 1) + nodegroup = request.DATA.get("nodegroup", None) + + return magnum.cluster_upgrade( + request, cluster_id, cluster_template, + max_batch_size=max_batch_size, nodegroup=nodegroup) + + @urls.register class Clusters(generic.View): """API for Magnum Clusters""" diff --git a/magnum_ui/static/dashboard/container-infra/clusters/actions.module.js b/magnum_ui/static/dashboard/container-infra/clusters/actions.module.js index 9a27d07a..f30c720a 100644 --- a/magnum_ui/static/dashboard/container-infra/clusters/actions.module.js +++ b/magnum_ui/static/dashboard/container-infra/clusters/actions.module.js @@ -36,6 +36,7 @@ 'horizon.dashboard.container-infra.clusters.delete.service', 'horizon.dashboard.container-infra.clusters.resize.service', 'horizon.dashboard.container-infra.clusters.update.service', + 'horizon.dashboard.container-infra.clusters.rolling-upgrade.service', 'horizon.dashboard.container-infra.clusters.show-certificate.service', 'horizon.dashboard.container-infra.clusters.sign-certificate.service', 'horizon.dashboard.container-infra.clusters.rotate-certificate.service', @@ -49,6 +50,7 @@ deleteClusterService, resizeClusterService, updateClusterService, + rollingUpgradeClusterService, showCertificateService, signCertificateService, rotateCertificateService, @@ -111,6 +113,13 @@ text: gettext('Update Cluster') } }) + .append({ + id: 'rollingUpgradeClusterAction', + service: rollingUpgradeClusterService, + template: { + text: gettext('Rolling Cluster Upgrade') + } + }) .append({ id: 'deleteClusterAction', service: deleteClusterService, diff --git a/magnum_ui/static/dashboard/container-infra/clusters/rolling-upgrade/upgrade.service.js b/magnum_ui/static/dashboard/container-infra/clusters/rolling-upgrade/upgrade.service.js new file mode 100644 index 00000000..66098290 --- /dev/null +++ b/magnum_ui/static/dashboard/container-infra/clusters/rolling-upgrade/upgrade.service.js @@ -0,0 +1,227 @@ +/** + * Copyright 2017 NEC Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @name horizon.dashboard.container-infra.clusters.rolling-upgrade.service + * @description Service for the container-infra cluster rolling upgrade modal. + * Allows user to choose a Cluster template with higher version number the + * cluster should upgrade to. Optionally, the number of nodes in a single + * upgrade batch can be chosen. + */ + angular + .module('horizon.dashboard.container-infra.clusters') + .factory('horizon.dashboard.container-infra.clusters.rolling-upgrade.service', upgradeService); + + upgradeService.$inject = [ + '$q', + '$document', + 'horizon.app.core.openstack-service-api.magnum', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service', + 'horizon.framework.widgets.modal-wait-spinner.service', + 'horizon.dashboard.container-infra.clusters.resourceType', + 'horizon.dashboard.container-infra.utils.service' + ]; + + function upgradeService( + $q, $document, magnum, actionResult, gettext, $qExtensions, modal, toast, spinnerModal, + resourceType, utils + ) { + + var modalConfig, formModel, isLatestTemplate, clusterTemplatesTitleMap; + + var service = { + perform: perform, + allowed: allowed + }; + + return service; + + ////////////// + + function perform(selected, $scope) { + // Simulate a click to dismiss opened action dropdown, otherwise it could interfere with + // correct behaviour of other dropdowns. + $document[0].body.click(); + + var deferred = $q.defer(); + spinnerModal.showModalSpinner(gettext('Loading')); + + var activeTemplateVersion, activeTemplateId; + + magnum.getCluster(selected.id).then(function(response) { + formModel = getFormModelDefaults(); + formModel.id = selected.id; + clusterTemplatesTitleMap = [ + // Default should have 2 optiosn (1+1 the default) + expect(modalConfig.form[0].titleMap.length).toBe(2); + }, 0); + + $timeout.flush(); + $scope.$apply(); + })); + + it('should not open the modal due to a request error and should hide the loading spinner', + inject(function($timeout) { + deferred = $q.defer(); + deferred.reject(); + spyOn(magnum, 'getCluster').and.returnValue(deferred.promise); + spyOn(magnum, 'getClusterTemplates').and.returnValue(deferred.promise); + + service.perform(selected, $scope); + + $timeout(function() { + expect(modal.open).not.toHaveBeenCalled(); + expect(spinnerModal.showModalSpinner).toHaveBeenCalled(); + expect(spinnerModal.hideModalSpinner).toHaveBeenCalled(); + }, 0); + + $timeout.flush(); + $scope.$apply(); + })); + + }); +})(); diff --git a/magnum_ui/static/dashboard/container-infra/magnum.service.js b/magnum_ui/static/dashboard/container-infra/magnum.service.js index 2c03e3a7..f0bb2bc8 100644 --- a/magnum_ui/static/dashboard/container-infra/magnum.service.js +++ b/magnum_ui/static/dashboard/container-infra/magnum.service.js @@ -31,6 +31,7 @@ var service = { createCluster: createCluster, updateCluster: updateCluster, + upgradeCluster: upgradeCluster, getCluster: getCluster, getClusters: getClusters, getClusterNodes: getClusterNodes, @@ -75,6 +76,13 @@ }); } + function upgradeCluster(id, params) { + return apiService.post('/api/container_infra/clusters/' + id + '/upgrade', params) + .error(function() { + toastService.add('error', gettext('Unable to perform rolling upgrade.')); + }); + } + function getCluster(id) { return apiService.get('/api/container_infra/clusters/' + id) .error(function() { @@ -145,8 +153,9 @@ }); } - function getClusterTemplates() { - return apiService.get('/api/container_infra/cluster_templates/') + function getClusterTemplates(relatedTemplateId) { + var filterQuery = relatedTemplateId ? '?related_to=' + relatedTemplateId : ''; + return apiService.get('/api/container_infra/cluster_templates/' + filterQuery) .error(function() { toastService.add('error', gettext('Unable to retrieve the cluster templates.')); }); diff --git a/magnum_ui/static/dashboard/container-infra/magnum.service.spec.js b/magnum_ui/static/dashboard/container-infra/magnum.service.spec.js index cb3f59dd..af75277e 100644 --- a/magnum_ui/static/dashboard/container-infra/magnum.service.spec.js +++ b/magnum_ui/static/dashboard/container-infra/magnum.service.spec.js @@ -102,6 +102,25 @@ "error": "Unable to delete the cluster with id: 1", "testInput": [1] }, + { + "func": "upgradeCluster", + "method": "post", + "path": "/api/container_infra/clusters/123/upgrade", + "data": { + "cluster_template": "ABC", + "max_batch_size": 1, + "node_group": "production_group" + }, + "error": "Unable to perform rolling upgrade.", + "testInput": [ + "123", + { + "cluster_template": "ABC", + "max_batch_size": 1, + "node_group": "production_group" + } + ] + }, { "func": "deleteClusters", "method": "delete", @@ -139,6 +158,13 @@ "path": "/api/container_infra/cluster_templates/", "error": "Unable to retrieve the cluster templates." }, + { + "func": "getClusterTemplates", + "method": "get", + "path": "/api/container_infra/cluster_templates/?related_to=123", + "error": "Unable to retrieve the cluster templates.", + "testInput": [123] + }, { "func": "deleteClusterTemplate", "method": "delete", diff --git a/magnum_ui/static/dashboard/container-infra/utils.service.js b/magnum_ui/static/dashboard/container-infra/utils.service.js new file mode 100644 index 00000000..7d69fc44 --- /dev/null +++ b/magnum_ui/static/dashboard/container-infra/utils.service.js @@ -0,0 +1,75 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +(function() { + "use strict"; + + angular.module('horizon.dashboard.container-infra') + .factory('horizon.dashboard.container-infra.utils.service', + utilsService); + + /* + * @ngdoc factory + * @name horizon.dashboard.container-infra.utils.service + * + * @description + * A utility service providing helpers for the Magnum UI frontend. + */ + function utilsService() { + return { + versionCompare: versionCompare + }; + + function versionCompare(v1, v2, options) { + var lexicographical = options && options.lexicographical; + var zeroExtend = options && options.zeroExtend; + var v1parts = v1.split('.'); + var v2parts = v2.split('.'); + + function isValidPart(x) { + return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); + } + + if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { + return NaN; + } + + if (zeroExtend) { + while (v1parts.length < v2parts.length) { v1parts.push("0"); } + while (v2parts.length < v1parts.length) { v2parts.push("0"); } + } + + if (!lexicographical) { + v1parts = v1parts.map(Number); + v2parts = v2parts.map(Number); + } + + for (var i = 0; i < v1parts.length; ++i) { + if (v2parts.length === i) { return 1; } + + if (v1parts[i] === v2parts[i]) { + continue; + } else if (v1parts[i] > v2parts[i]) { + return 1; + } else { + return -1; + } + } + + if (v1parts.length !== v2parts.length) { return -1; } + + return 0; + } + } +})(); diff --git a/magnum_ui/static/dashboard/container-infra/utils.service.spec.js b/magnum_ui/static/dashboard/container-infra/utils.service.spec.js new file mode 100644 index 00000000..e580dcfe --- /dev/null +++ b/magnum_ui/static/dashboard/container-infra/utils.service.spec.js @@ -0,0 +1,42 @@ +/** + * Copyright 2017 NEC Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +(function() { + 'use strict'; + + describe('horizon.dashboard.container-infra.utils.service', function() { + + var service; + + /////////////////// + + beforeEach(module('horizon.dashboard.container-infra')); + beforeEach(inject(function($injector) { + service = $injector.get( + 'horizon.dashboard.container-infra.utils.service'); + })); + + it('should compare two semver-based versions strings', function() { + expect(service.versionCompare('1.2.2','1.2.2')).toBe(0); + expect(service.versionCompare('1.2.3','1.2.2')).toBe(1); + expect(service.versionCompare('1.2.2','1.2.3')).toBe(-1); + + expect(service.versionCompare('1.12.2','1.2.2')).toBe(1); + expect(service.versionCompare('12.1.2','1.3.2')).toBe(1); + expect(service.versionCompare('1.3.2','1.3.11')).toBe(-1); + }); + }); +})(); diff --git a/releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml b/releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml new file mode 100644 index 00000000..bac50133 --- /dev/null +++ b/releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds REST api and Angular service for rolling upgrade action on cluster.