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 Change-Id: Id3fd3ee80fb27b08673933800aea6e7ee7ac7cd0
This commit is contained in:
parent
db0803fe55
commit
cd0817a13b
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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,22 @@ class ClusterResize(generic.View):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
class ClusterUpgrade(generic.View):
|
||||
|
||||
url_regex = r'container_infra/clusters/(?P<cluster_id>[^/]+)/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"""
|
||||
|
@ -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,
|
||||
|
@ -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 <select> placeholder
|
||||
{
|
||||
value:'',
|
||||
name: gettext("Choose a Cluster Template to upgrade to")
|
||||
}
|
||||
];
|
||||
|
||||
processClusterResponse(response.data);
|
||||
|
||||
// Retrieve only cluster templates related to the current one.
|
||||
return magnum.getClusterTemplates(activeTemplateId);
|
||||
}).then(function(response) {
|
||||
processClusterTemplatesResponse(response.data.items);
|
||||
|
||||
modalConfig = createModalConfig();
|
||||
|
||||
deferred.resolve(modal.open(modalConfig).then(onModalSubmit));
|
||||
spinnerModal.hideModalSpinner();
|
||||
|
||||
$scope.model = formModel;
|
||||
}).catch(onError);
|
||||
|
||||
function processClusterResponse(cluster) {
|
||||
formModel.master_nodes = cluster.master_count;
|
||||
formModel.worker_nodes = cluster.node_count;
|
||||
|
||||
activeTemplateVersion = cluster.labels.kube_tag;
|
||||
activeTemplateId = cluster.cluster_template_id;
|
||||
}
|
||||
|
||||
function processClusterTemplatesResponse(clusterTemplates) {
|
||||
if (!clusterTemplates) { return; }
|
||||
|
||||
var startingTemplatesTitleMapLength = clusterTemplatesTitleMap.length;
|
||||
|
||||
// Only load templates that are greater than the current template (kube tag comparison)
|
||||
clusterTemplates.forEach(function(template) {
|
||||
if (isVersionGreater(activeTemplateVersion, template.labels.kube_tag)) {
|
||||
clusterTemplatesTitleMap.push({
|
||||
value: template.id,
|
||||
name: template.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Order templates by name in descending order
|
||||
clusterTemplatesTitleMap.sort(function(firstTemplate, secondTemplate) {
|
||||
return firstTemplate.name < secondTemplate.name ? 1 : -1;
|
||||
});
|
||||
|
||||
// If nothing has been added to the map => already on latest template
|
||||
isLatestTemplate = startingTemplatesTitleMapLength === clusterTemplatesTitleMap.length;
|
||||
}
|
||||
|
||||
function onError(err) {
|
||||
spinnerModal.hideModalSpinner();
|
||||
return deferred.reject(err);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function createModalConfig() {
|
||||
return {
|
||||
title: gettext('Rolling Cluster Upgrade'),
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
'cluster_template_id': {
|
||||
title: gettext('New Cluster Template'),
|
||||
type: 'string'
|
||||
},
|
||||
'max_batch_size': {
|
||||
title: gettext('Maximum Batch Size'),
|
||||
type: 'number',
|
||||
minimum: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
form: [
|
||||
{
|
||||
key: 'cluster_template_id',
|
||||
type: 'select',
|
||||
titleMap: clusterTemplatesTitleMap,
|
||||
required: true,
|
||||
readonly: isLatestTemplate,
|
||||
description: isLatestTemplate
|
||||
? gettext('<em>This cluster is already on the latest compatible template</em>') : null
|
||||
},
|
||||
{
|
||||
key: 'max_batch_size',
|
||||
placeholder: gettext('The cluster node count.'),
|
||||
// Disable if there's nothing to upgrade or if the the default value incrementation
|
||||
// would fail the validation.
|
||||
readonly: isLatestTemplate ||
|
||||
!isBatchSizeValid(getFormModelDefaults().max_batch_size + 1),
|
||||
validationMessage: {
|
||||
sizeExceeded: gettext('The maximum number of nodes in the batch has been exceeded.'),
|
||||
101: gettext('A batch cannot have less than one node.')
|
||||
},
|
||||
$validators: {
|
||||
sizeExceeded: isBatchSizeValid
|
||||
}
|
||||
}
|
||||
],
|
||||
model: formModel
|
||||
};
|
||||
}
|
||||
|
||||
function getFormModelDefaults() {
|
||||
return {
|
||||
cluster_template_id: '',
|
||||
max_batch_size: 1
|
||||
};
|
||||
}
|
||||
|
||||
function allowed() {
|
||||
return $qExtensions.booleanAsPromise(true);
|
||||
}
|
||||
|
||||
function isBatchSizeValid(batchSize) {
|
||||
return batchSize &&
|
||||
(batchSize === 1 ||
|
||||
batchSize <= formModel.master_nodes / 3 && batchSize <= formModel.worker_nodes / 5);
|
||||
}
|
||||
|
||||
function onModalSubmit() {
|
||||
return magnum.upgradeCluster(formModel.id, {
|
||||
cluster_template: formModel.cluster_template_id,
|
||||
max_batch_size: formModel.max_batch_size,
|
||||
nodegroup: 'production_group'
|
||||
}).then(onRequestSuccess);
|
||||
}
|
||||
|
||||
function onRequestSuccess() {
|
||||
toast.add('success', gettext('Cluster is being upgraded to the new Cluster template'));
|
||||
return actionResult.getActionResult()
|
||||
.updated(resourceType, formModel.id)
|
||||
.result;
|
||||
}
|
||||
|
||||
function isVersionGreater(v1, v2) {
|
||||
if (!v1 || !v2) { return null; }
|
||||
|
||||
// Strip the 'v' if prefixed in the version
|
||||
if (v1[0] === 'v') { v1 = v1.substr(1); }
|
||||
if (v2[0] === 'v') { v2 = v2.substr(1); }
|
||||
return utils.versionCompare(v1, v2) < 0;
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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.clusters.rolling-upgrade.service', function() {
|
||||
|
||||
var service, $scope, $q, deferred, magnum, spinnerModal, modalConfig;
|
||||
var selected = {
|
||||
id: 1
|
||||
};
|
||||
var modal = {
|
||||
open: function(config) {
|
||||
modalConfig = config;
|
||||
deferred = $q.defer();
|
||||
deferred.resolve(config);
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////
|
||||
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.dashboard.container-infra.clusters'));
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('horizon.framework.widgets.form.ModalFormService', modal);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
||||
$q = _$q_;
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get(
|
||||
'horizon.dashboard.container-infra.clusters.rolling-upgrade.service');
|
||||
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||
spinnerModal = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
|
||||
|
||||
spyOn(spinnerModal, 'showModalSpinner').and.callFake(function() {});
|
||||
spyOn(spinnerModal, 'hideModalSpinner').and.callFake(function() {});
|
||||
|
||||
deferred = $q.defer();
|
||||
deferred.resolve({data: {uuid: 1, labels: "key1:val1,key2:val2"}});
|
||||
spyOn(magnum, 'upgradeCluster').and.returnValue(deferred.promise);
|
||||
|
||||
spyOn(modal, 'open').and.callThrough();
|
||||
}));
|
||||
|
||||
it('should check the policy if the user is allowed to update cluster', function() {
|
||||
var allowed = service.allowed();
|
||||
expect(allowed).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open the modal, hide the loading spinner and check the form model',
|
||||
inject(function($timeout) {
|
||||
var mockClusterDetails = {
|
||||
data: {
|
||||
master_count: 1,
|
||||
node_count: 2,
|
||||
labels: { kube_tag: 'v1.3.4' }
|
||||
}
|
||||
};
|
||||
|
||||
var mockClusterTemplates = {
|
||||
data: {
|
||||
items: [
|
||||
{ labels: { kube_tag: 'v1.4.1' } },
|
||||
{ labels: { kube_tag: 'v1.3.4' } }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
deferred = $q.defer();
|
||||
deferred.resolve(mockClusterDetails);
|
||||
spyOn(magnum, 'getCluster').and.returnValue(deferred.promise);
|
||||
|
||||
deferred = $q.defer();
|
||||
deferred.resolve(mockClusterTemplates);
|
||||
spyOn(magnum, 'getClusterTemplates').and.returnValue(deferred.promise);
|
||||
|
||||
service.perform(selected, $scope);
|
||||
|
||||
$timeout(function() {
|
||||
expect(modal.open).toHaveBeenCalled();
|
||||
expect(spinnerModal.showModalSpinner).toHaveBeenCalled();
|
||||
expect(spinnerModal.hideModalSpinner).toHaveBeenCalled();
|
||||
|
||||
// Check if the form's model skeleton is correct
|
||||
expect(modalConfig.model.id).toBe(selected.id);
|
||||
expect(modalConfig.model.master_nodes).toBe(mockClusterDetails.data.master_count);
|
||||
expect(modalConfig.model.worker_nodes).toBe(mockClusterDetails.data.node_count);
|
||||
expect(modalConfig.title).toBeDefined();
|
||||
expect(modalConfig.schema).toBeDefined();
|
||||
expect(modalConfig.form).toBeDefined();
|
||||
|
||||
// Only one version is greater than `v1.3.4`, so the
|
||||
// form <select> 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();
|
||||
}));
|
||||
|
||||
});
|
||||
})();
|
@ -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.'));
|
||||
});
|
||||
|
@ -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",
|
||||
|
75
magnum_ui/static/dashboard/container-infra/utils.service.js
Normal file
75
magnum_ui/static/dashboard/container-infra/utils.service.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
})();
|
@ -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);
|
||||
});
|
||||
});
|
||||
})();
|
4
releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml
Normal file
4
releasenotes/notes/upgrade-actions-adf2f749ec0cc817.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds REST api and Angular service for rolling upgrade action on cluster.
|
Loading…
x
Reference in New Issue
Block a user