Merge "Improve cluster launch workflow"
This commit is contained in:
commit
c70e807c54
@ -31,6 +31,90 @@ template that will not work based on their current template type.
|
|||||||
This filtering is only relevant when choosing a new template for
|
This filtering is only relevant when choosing a new template for
|
||||||
upgrading a cluster.
|
upgrading a cluster.
|
||||||
|
|
||||||
|
|
||||||
|
MAGNUM_INGRESS_CONTROLLERS
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 5.3.0 (Ussuri)
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
MAGNUM_INGRESS_CONTROLLERS = [
|
||||||
|
{
|
||||||
|
"name": "NGINX",
|
||||||
|
"labels": {
|
||||||
|
"ingress_controller": "nginx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Traefik",
|
||||||
|
"labels": {
|
||||||
|
"ingress_controller": "traefik"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Octavia",
|
||||||
|
"labels": {
|
||||||
|
"ingress_controller": "octavia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
This setting specifies which `Kubernetes Ingress Controllers <https://docs.openstack.org/horizon/latest/configuration/index.html>`__
|
||||||
|
are supported by the deployed version of magnum and map directly to the
|
||||||
|
response returned by the magnum-ui `api/container-infra/ingress_controllers` endpoint.
|
||||||
|
|
||||||
|
MAGNUM_AVAILABLE_ADDONS
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. versionadded:: 5.3.0 (Ussuri)
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
MAGNUM_AVAILABLE_ADDONS = [
|
||||||
|
{
|
||||||
|
"name": "Kubernetes Dashboard",
|
||||||
|
"selected": True,
|
||||||
|
"labels": {
|
||||||
|
"kube_dashboard_enabled": True
|
||||||
|
},
|
||||||
|
"labels_unselected": {
|
||||||
|
"kube_dashboard_enabled": False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Influx Grafana Dashboard",
|
||||||
|
"selected": False,
|
||||||
|
"labels": {
|
||||||
|
"influx_grafana_dashboard_enabled": True
|
||||||
|
},
|
||||||
|
"labels_unselected": {
|
||||||
|
"influx_grafana_dashboard_enabled": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Specifies which 'Addon Software' is available or supported in the deployed version
|
||||||
|
of magnum and specifies which labels need to be included in order to enable or
|
||||||
|
disable the Software Addon.
|
||||||
|
|
||||||
|
Examples of `Addon Software` include but are not limited to:
|
||||||
|
|
||||||
|
* `Kubernetes Dashboard <https://docs.openstack.org/magnum/latest/user/index.html#kube-dashboard-enabled>`__
|
||||||
|
* `Influx Grafana Dashboard <https://docs.openstack.org/magnum/train/user/index.html#influx-grafana-dashboard-enabled>`__
|
||||||
|
|
||||||
|
Values specified in the ``MAGNUM_AVAILABLE_ADDONS`` setting map directly to the
|
||||||
|
values returned in the response of the `api/container-infra/available_addons`
|
||||||
|
endpoint.
|
||||||
|
|
||||||
Horizon Settings
|
Horizon Settings
|
||||||
================
|
================
|
||||||
|
|
||||||
@ -38,4 +122,3 @@ For more configurations, see
|
|||||||
`Configuration Guide
|
`Configuration Guide
|
||||||
<https://docs.openstack.org/horizon/latest/configuration/index.html>`__
|
<https://docs.openstack.org/horizon/latest/configuration/index.html>`__
|
||||||
in the Horizon documentation.
|
in the Horizon documentation.
|
||||||
|
|
||||||
|
@ -12,9 +12,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.http import HttpResponseNotFound
|
from django.http import HttpResponseNotFound
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
@ -26,7 +30,7 @@ from openstack_dashboard.api import neutron
|
|||||||
from openstack_dashboard.api.rest import urls
|
from openstack_dashboard.api.rest import urls
|
||||||
from openstack_dashboard.api.rest import utils as rest_utils
|
from openstack_dashboard.api.rest import utils as rest_utils
|
||||||
|
|
||||||
import re
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def change_to_id(obj):
|
def change_to_id(obj):
|
||||||
@ -39,6 +43,59 @@ def change_to_id(obj):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@urls.register
|
||||||
|
class IngressControllers(generic.View):
|
||||||
|
url_regex = r'container_infra/ingress_controllers/'
|
||||||
|
|
||||||
|
@rest_utils.ajax()
|
||||||
|
def get(self, request):
|
||||||
|
configured_controllers = getattr(
|
||||||
|
settings, "MAGNUM_INGRESS_CONTROLLERS", [])
|
||||||
|
available_controllers = []
|
||||||
|
|
||||||
|
for controller in configured_controllers:
|
||||||
|
try:
|
||||||
|
parsed = {}
|
||||||
|
parsed["name"] = controller["name"]
|
||||||
|
parsed["labels"] = controller["labels"]
|
||||||
|
assert type(parsed["labels"]) is dict
|
||||||
|
available_controllers.append(parsed)
|
||||||
|
except KeyError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
except AssertionError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
|
||||||
|
return {"controllers": available_controllers}
|
||||||
|
|
||||||
|
|
||||||
|
@urls.register
|
||||||
|
class Addons(generic.View):
|
||||||
|
url_regex = r'container_infra/available_addons/'
|
||||||
|
|
||||||
|
@rest_utils.ajax()
|
||||||
|
def get(self, request):
|
||||||
|
available_addons = []
|
||||||
|
|
||||||
|
configured_addons = getattr(
|
||||||
|
settings, "MAGNUM_AVAILABLE_ADDONS", [])
|
||||||
|
|
||||||
|
for configured_addon in configured_addons:
|
||||||
|
addon = {}
|
||||||
|
try:
|
||||||
|
addon["name"] = configured_addon["name"]
|
||||||
|
addon["selected"] = configured_addon["selected"]
|
||||||
|
assert type(addon["selected"]) is bool
|
||||||
|
addon["labels"] = configured_addon["labels"]
|
||||||
|
assert type(addon["labels"]) is dict
|
||||||
|
available_addons.append(addon)
|
||||||
|
except KeyError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
except AssertionError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
|
||||||
|
return {"addons": available_addons}
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class ClusterTemplate(generic.View):
|
class ClusterTemplate(generic.View):
|
||||||
"""API for retrieving a single cluster template"""
|
"""API for retrieving a single cluster template"""
|
||||||
@ -318,7 +375,13 @@ class Quota(generic.View):
|
|||||||
@rest_utils.ajax()
|
@rest_utils.ajax()
|
||||||
def get(self, request, project_id, resource):
|
def get(self, request, project_id, resource):
|
||||||
"""Get a specific quota"""
|
"""Get a specific quota"""
|
||||||
return magnum.quotas_show(request, project_id, resource).to_dict()
|
try:
|
||||||
|
return magnum.quotas_show(request, project_id, resource).to_dict()
|
||||||
|
except AttributeError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
message = ("Quota could not be found: "
|
||||||
|
"project_id %s resource %s" % (project_id, resource))
|
||||||
|
return HttpResponse(message, status=404)
|
||||||
|
|
||||||
@rest_utils.ajax(data_required=True)
|
@rest_utils.ajax(data_required=True)
|
||||||
def patch(self, request, project_id, resource):
|
def patch(self, request, project_id, resource):
|
||||||
|
@ -35,7 +35,6 @@
|
|||||||
'horizon.dashboard.container-infra.clusters.create.service',
|
'horizon.dashboard.container-infra.clusters.create.service',
|
||||||
'horizon.dashboard.container-infra.clusters.delete.service',
|
'horizon.dashboard.container-infra.clusters.delete.service',
|
||||||
'horizon.dashboard.container-infra.clusters.resize.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.rolling-upgrade.service',
|
||||||
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
||||||
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
||||||
@ -49,7 +48,6 @@
|
|||||||
createClusterService,
|
createClusterService,
|
||||||
deleteClusterService,
|
deleteClusterService,
|
||||||
resizeClusterService,
|
resizeClusterService,
|
||||||
updateClusterService,
|
|
||||||
rollingUpgradeClusterService,
|
rollingUpgradeClusterService,
|
||||||
showCertificateService,
|
showCertificateService,
|
||||||
signCertificateService,
|
signCertificateService,
|
||||||
@ -106,13 +104,6 @@
|
|||||||
text: gettext('Resize Cluster')
|
text: gettext('Resize Cluster')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.append({
|
|
||||||
id: 'updateClusterAction',
|
|
||||||
service: updateClusterService,
|
|
||||||
template: {
|
|
||||||
text: gettext('Update Cluster')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.append({
|
.append({
|
||||||
id: 'rollingUpgradeClusterAction',
|
id: 'rollingUpgradeClusterAction',
|
||||||
service: rollingUpgradeClusterService,
|
service: rollingUpgradeClusterService,
|
||||||
|
@ -55,11 +55,6 @@
|
|||||||
expect(actionHasId(actions, 'resizeClusterAction')).toBe(true);
|
expect(actionHasId(actions, 'resizeClusterAction')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registers Update Cluster as an item action', function() {
|
|
||||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
|
||||||
expect(actionHasId(actions, 'updateClusterAction')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('registers Delete Cluster as an item action', function() {
|
it('registers Delete Cluster as an item action', function() {
|
||||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
expect(actionHasId(actions, 'deleteClusterAction')).toBe(true);
|
expect(actionHasId(actions, 'deleteClusterAction')).toBe(true);
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
/**
|
/**
|
||||||
* @ngdoc overview
|
* @ngdoc overview
|
||||||
* @name horizon.dashboard.container-infra.clusters.create.service
|
* @name horizon.dashboard.container-infra.clusters.create.service
|
||||||
* @description Service for the container-infra cluster create modal
|
* @description Service for the container-infra 'Create New Cluster' dialog.
|
||||||
|
* Also responsible for processing the user submission.
|
||||||
*/
|
*/
|
||||||
angular
|
angular
|
||||||
.module('horizon.dashboard.container-infra.clusters')
|
.module('horizon.dashboard.container-infra.clusters')
|
||||||
@ -27,22 +28,22 @@
|
|||||||
createService.$inject = [
|
createService.$inject = [
|
||||||
'$location',
|
'$location',
|
||||||
'horizon.app.core.openstack-service-api.magnum',
|
'horizon.app.core.openstack-service-api.magnum',
|
||||||
'horizon.app.core.openstack-service-api.policy',
|
|
||||||
'horizon.framework.util.actions.action-result.service',
|
'horizon.framework.util.actions.action-result.service',
|
||||||
'horizon.framework.util.i18n.gettext',
|
'horizon.framework.util.i18n.gettext',
|
||||||
'horizon.framework.util.q.extensions',
|
'horizon.framework.util.q.extensions',
|
||||||
'horizon.framework.widgets.form.ModalFormService',
|
'horizon.framework.widgets.form.ModalFormService',
|
||||||
'horizon.framework.widgets.toast.service',
|
'horizon.framework.widgets.toast.service',
|
||||||
|
'horizon.framework.widgets.modal-wait-spinner.service',
|
||||||
'horizon.dashboard.container-infra.clusters.resourceType',
|
'horizon.dashboard.container-infra.clusters.resourceType',
|
||||||
'horizon.dashboard.container-infra.clusters.workflow'
|
'horizon.dashboard.container-infra.clusters.workflow'
|
||||||
];
|
];
|
||||||
|
|
||||||
function createService(
|
function createService(
|
||||||
$location, magnum, policy, actionResult, gettext, $qExtensions, modal, toast,
|
$location, magnum, actionResult, gettext, $qExtensions, modal, toast, spinnerModal,
|
||||||
resourceType, workflow
|
resourceType, workflow
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var config;
|
var modalConfig;
|
||||||
var message = {
|
var message = {
|
||||||
success: gettext('Cluster %s was successfully created.')
|
success: gettext('Cluster %s was successfully created.')
|
||||||
};
|
};
|
||||||
@ -57,44 +58,133 @@
|
|||||||
//////////////
|
//////////////
|
||||||
|
|
||||||
function perform(selected, $scope) {
|
function perform(selected, $scope) {
|
||||||
config = workflow.init('create', gettext('Create'), $scope);
|
spinnerModal.showModalSpinner(gettext('Loading'));
|
||||||
if (typeof selected !== 'undefined') {
|
|
||||||
config.model.cluster_template_id = selected.id;
|
function onCreateWorkflowConfig(config) {
|
||||||
|
modalConfig = config;
|
||||||
|
spinnerModal.hideModalSpinner();
|
||||||
|
return modal.open(modalConfig).then(onModalSubmit);
|
||||||
}
|
}
|
||||||
return modal.open(config).then(submit);
|
|
||||||
|
return workflow.init(gettext('Create New Cluster'), $scope)
|
||||||
|
.then(onCreateWorkflowConfig)
|
||||||
|
.catch(hideSpinnerOnError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSpinnerOnError(error) {
|
||||||
|
spinnerModal.hideModalSpinner();
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
function allowed() {
|
function allowed() {
|
||||||
return $qExtensions.booleanAsPromise(true);
|
return $qExtensions.booleanAsPromise(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit(context) {
|
function onModalSubmit(context) {
|
||||||
context.model = cleanNullProperties(context.model);
|
return magnum.createCluster(buildRequestObject(context.model), false)
|
||||||
return magnum.createCluster(context.model, false).then(success, true);
|
.then(onRequestSuccess, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanNullProperties(model) {
|
function buildRequestObject(model) {
|
||||||
// Initially clean fields that don't have any value.
|
var MODEL_DEFAULTS = model.DEFAULTS;
|
||||||
// Not only "null", blank too.
|
var requestLabels = {};
|
||||||
for (var key in model) {
|
|
||||||
if (model.hasOwnProperty(key) && model[key] === null || model[key] === "" ||
|
var requestObject = {
|
||||||
key === "tabs") {
|
// Defaults required by the endpoint
|
||||||
delete model[key];
|
discovery_url: null,
|
||||||
|
create_timeout: 60,
|
||||||
|
rollback: false,
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
name: model.name,
|
||||||
|
cluster_template_id: model.cluster_template_id,
|
||||||
|
keypair: model.keypair,
|
||||||
|
floating_ip_enabled: model.floating_ip_enabled,
|
||||||
|
labels: requestLabels
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional request fields
|
||||||
|
addFieldToRequestObjectIfSet('master_count','master_count');
|
||||||
|
addFieldToRequestObjectIfSet('master_flavor_id','master_flavor_id');
|
||||||
|
addFieldToRequestObjectIfSet('node_count','node_count');
|
||||||
|
addFieldToRequestObjectIfSet('flavor_id','flavor_id');
|
||||||
|
|
||||||
|
if (!model.create_network) {
|
||||||
|
addFieldToRequestObjectIfSet('fixed_network','fixed_network');
|
||||||
|
}
|
||||||
|
// Labels processing order (the following overrides previous):
|
||||||
|
// Cluster Templates -> Create Form -> User-defined in 'labels' textarea
|
||||||
|
|
||||||
|
// 1) Cluster Templates labels
|
||||||
|
if (model.templateLabels) {
|
||||||
|
angular.extend(requestLabels, model.templateLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Create Workflow Form labels
|
||||||
|
requestLabels.availability_zone = model.availability_zone;
|
||||||
|
requestLabels.auto_scaling_enabled = model.auto_scaling_enabled;
|
||||||
|
requestLabels.auto_healing_enabled = model.auto_healing_enabled;
|
||||||
|
|
||||||
|
if (model.auto_scaling_enabled) {
|
||||||
|
requestLabels.min_node_count = model.min_node_count;
|
||||||
|
requestLabels.max_node_count = model.max_node_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2A) Labels from user-selected addons
|
||||||
|
angular.forEach(model.addons, function(addon) {
|
||||||
|
angular.extend(requestLabels, addon.labels);
|
||||||
|
});
|
||||||
|
// 2B) Labels from user-selected ingress controller
|
||||||
|
if (model.ingress_controller && model.ingress_controller.labels) {
|
||||||
|
angular.extend(requestLabels, model.ingress_controller.labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) User-defined Custom labels
|
||||||
|
// Parse all labels comma-separated key=value pairs and inject them into request object
|
||||||
|
if (model.labels !== MODEL_DEFAULTS.labels) {
|
||||||
|
try {
|
||||||
|
model.labels.split(',').forEach(function(kvPair) {
|
||||||
|
var pairsList = kvPair.split('=');
|
||||||
|
|
||||||
|
// Remove leading and trailing whitespaces & convert to l-case
|
||||||
|
var labelKey = pairsList[0].trim().toLowerCase();
|
||||||
|
var labelValue = pairsList[1].trim().toLowerCase();
|
||||||
|
|
||||||
|
if (labelValue) {
|
||||||
|
// Only override existing label values if user override flag is true
|
||||||
|
if (!requestLabels.hasOwnProperty(labelKey) || model.override_labels) {
|
||||||
|
requestLabels[labelKey] = labelValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast.add('error', gettext('Unable to process `Additional Labels`. ' +
|
||||||
|
'Not all labels will be applied.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return model;
|
|
||||||
|
// Only add to the request Object if set (= not default)
|
||||||
|
function addFieldToRequestObjectIfSet(requestFieldName, modelFieldName) {
|
||||||
|
if (model[modelFieldName] !== MODEL_DEFAULTS[modelFieldName]) {
|
||||||
|
requestObject[requestFieldName] = model[modelFieldName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function success(response) {
|
function onRequestSuccess(response) {
|
||||||
response.data.id = response.data.uuid;
|
response.data.id = response.data.uuid;
|
||||||
toast.add('success', interpolate(message.success, [response.data.id]));
|
toast.add('success', interpolate(message.success, [response.data.id]));
|
||||||
|
|
||||||
var result = actionResult.getActionResult()
|
var result = actionResult.getActionResult()
|
||||||
.created(resourceType, response.data.id);
|
.created(resourceType, response.data.id);
|
||||||
|
|
||||||
if (result.result.failed.length === 0 && result.result.created.length > 0) {
|
if (result.result.failed.length === 0 && result.result.created.length > 0) {
|
||||||
$location.path("/project/clusters");
|
$location.path('/project/clusters');
|
||||||
} else {
|
|
||||||
return result.result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -19,15 +19,26 @@
|
|||||||
|
|
||||||
describe('horizon.dashboard.container-infra.clusters.create.service', function() {
|
describe('horizon.dashboard.container-infra.clusters.create.service', function() {
|
||||||
|
|
||||||
var service, $scope, $q, deferred, magnum, workflow;
|
var service, $scope, $q, deferred, magnum, workflow, spinnerModal, modalConfig, configDeferred;
|
||||||
|
|
||||||
var model = {
|
var model = {
|
||||||
id: 1
|
id: 1,
|
||||||
|
labels: 'key1=value1,key2=value2',
|
||||||
|
auto_scaling_enabled: true,
|
||||||
|
templateLabels: {key1:'default value'},
|
||||||
|
override_labels: true,
|
||||||
|
master_count: 1,
|
||||||
|
create_network: true,
|
||||||
|
addons: [{labels:{}},{labels:{}}],
|
||||||
|
ingress_controller: {labels:{ingress_controller:''}},
|
||||||
|
DEFAULTS: {labels:''}
|
||||||
};
|
};
|
||||||
var modal = {
|
var modal = {
|
||||||
open: function(config) {
|
open: function(config) {
|
||||||
config.model = model;
|
|
||||||
deferred = $q.defer();
|
deferred = $q.defer();
|
||||||
deferred.resolve(config);
|
deferred.resolve(config);
|
||||||
|
modalConfig = config;
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -48,11 +59,25 @@
|
|||||||
service = $injector.get('horizon.dashboard.container-infra.clusters.create.service');
|
service = $injector.get('horizon.dashboard.container-infra.clusters.create.service');
|
||||||
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||||
workflow = $injector.get('horizon.dashboard.container-infra.clusters.workflow');
|
workflow = $injector.get('horizon.dashboard.container-infra.clusters.workflow');
|
||||||
|
|
||||||
|
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 = $q.defer();
|
||||||
deferred.resolve({data: {uuid: 1}});
|
deferred.resolve({data: {uuid: 1}});
|
||||||
|
|
||||||
|
configDeferred = $q.defer();
|
||||||
|
configDeferred.resolve({
|
||||||
|
title: 'Create New Cluster',
|
||||||
|
schema: {},
|
||||||
|
form: {},
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
|
||||||
spyOn(magnum, 'createCluster').and.returnValue(deferred.promise);
|
spyOn(magnum, 'createCluster').and.returnValue(deferred.promise);
|
||||||
|
spyOn(workflow, 'init').and.returnValue(configDeferred.promise);
|
||||||
spyOn(modal, 'open').and.callThrough();
|
spyOn(modal, 'open').and.callThrough();
|
||||||
spyOn(workflow, 'init').and.returnValue({model: model});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should check the policy if the user is allowed to create cluster', function() {
|
it('should check the policy if the user is allowed to create cluster', function() {
|
||||||
@ -60,15 +85,36 @@
|
|||||||
expect(allowed).toBeTruthy();
|
expect(allowed).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('open the modal', inject(function($timeout) {
|
it('should open the modal, hide the loading spinner and have valid ' +
|
||||||
service.perform(model, $scope);
|
'form model', inject(function($timeout) {
|
||||||
|
service.perform(null, $scope);
|
||||||
|
|
||||||
expect(modal.open).toHaveBeenCalled();
|
$timeout(function() {
|
||||||
|
expect(modal.open).toHaveBeenCalled();
|
||||||
|
expect(magnum.createCluster).toHaveBeenCalled();
|
||||||
|
// Check if the form's model skeleton is correct
|
||||||
|
expect(modalConfig.model).toBeDefined();
|
||||||
|
expect(modalConfig.schema).toBeDefined();
|
||||||
|
expect(modalConfig.form).toBeDefined();
|
||||||
|
expect(modalConfig.title).toEqual('Create New Cluster');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$timeout.flush();
|
||||||
|
$scope.$apply();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not crash unexpectedly with empty form model', inject(function($timeout) {
|
||||||
|
model.auto_scaling_enabled = null;
|
||||||
|
model.templateLabels = null;
|
||||||
|
model.override_labels = null;
|
||||||
|
model.create_network = null;
|
||||||
|
model.addons = null;
|
||||||
|
model.labels = 'invalid label';
|
||||||
|
|
||||||
|
service.perform(null, $scope);
|
||||||
|
|
||||||
$timeout.flush();
|
$timeout.flush();
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
expect(magnum.createCluster).toHaveBeenCalled();
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.update.service
|
|
||||||
* @description Service for the container-infra cluster update modal
|
|
||||||
*/
|
|
||||||
angular
|
|
||||||
.module('horizon.dashboard.container-infra.clusters')
|
|
||||||
.factory('horizon.dashboard.container-infra.clusters.update.service', updateService);
|
|
||||||
|
|
||||||
updateService.$inject = [
|
|
||||||
'horizon.app.core.openstack-service-api.magnum',
|
|
||||||
'horizon.app.core.openstack-service-api.policy',
|
|
||||||
'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.dashboard.container-infra.clusters.resourceType',
|
|
||||||
'horizon.dashboard.container-infra.clusters.workflow'
|
|
||||||
];
|
|
||||||
|
|
||||||
function updateService(
|
|
||||||
magnum, policy, actionResult, gettext, $qExtensions, modal, toast, resourceType, workflow
|
|
||||||
) {
|
|
||||||
|
|
||||||
var config;
|
|
||||||
var message = {
|
|
||||||
success: gettext('Cluster %s was successfully updated.')
|
|
||||||
};
|
|
||||||
|
|
||||||
var service = {
|
|
||||||
perform: perform,
|
|
||||||
allowed: allowed
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
|
|
||||||
//////////////
|
|
||||||
|
|
||||||
function perform(selected, $scope) {
|
|
||||||
config = workflow.init('update', gettext('Update Cluster'), $scope);
|
|
||||||
config.model.id = selected.id;
|
|
||||||
|
|
||||||
// load current data
|
|
||||||
magnum.getCluster(selected.id).then(onLoad);
|
|
||||||
function onLoad(response) {
|
|
||||||
config.model.name = response.data.name
|
|
||||||
? response.data.name : "";
|
|
||||||
config.model.cluster_template_id = response.data.cluster_template_id
|
|
||||||
? response.data.cluster_template_id : "";
|
|
||||||
config.model.master_count = response.data.master_count
|
|
||||||
? response.data.master_count : null;
|
|
||||||
config.model.node_count = response.data.node_count
|
|
||||||
? response.data.node_count : null;
|
|
||||||
config.model.discovery_url = response.data.discovery_url
|
|
||||||
? response.data.discovery_url : "";
|
|
||||||
config.model.create_timeout = response.data.create_timeout
|
|
||||||
? response.data.create_timeout : null;
|
|
||||||
config.model.keypair = response.data.keypair
|
|
||||||
? response.data.keypair : "";
|
|
||||||
config.model.docker_volume_size = response.data.docker_volume_size
|
|
||||||
? response.data.docker_volume_size : "";
|
|
||||||
config.model.master_flavor_id = response.data.master_flavor_id
|
|
||||||
? response.data.master_flavor_id : "";
|
|
||||||
config.model.flavor_id = response.data.flavor_id
|
|
||||||
? response.data.flavor_id : "";
|
|
||||||
var labels = "";
|
|
||||||
for (var key in response.data.labels) {
|
|
||||||
if (response.data.labels.hasOwnProperty(key)) {
|
|
||||||
if (labels !== "") {
|
|
||||||
labels += ",";
|
|
||||||
}
|
|
||||||
labels += key + "=" + response.data.labels[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.model.labels = labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
return modal.open(config).then(submit);
|
|
||||||
}
|
|
||||||
|
|
||||||
function allowed() {
|
|
||||||
return $qExtensions.booleanAsPromise(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit(context) {
|
|
||||||
var id = context.model.id;
|
|
||||||
context.model = cleanNullProperties(context.model);
|
|
||||||
return magnum.updateCluster(id, context.model, true)
|
|
||||||
.then(success, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanNullProperties(model) {
|
|
||||||
// Initially clean fields that don't have any value.
|
|
||||||
// Not only "null", blank too.
|
|
||||||
for (var key in model) {
|
|
||||||
if (model.hasOwnProperty(key) && model[key] === null || model[key] === "" ||
|
|
||||||
key === "tabs" || key === "id") {
|
|
||||||
delete model[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
function success(response) {
|
|
||||||
response.data.id = response.data.uuid;
|
|
||||||
toast.add('success', interpolate(message.success, [response.data.id]));
|
|
||||||
return actionResult.getActionResult()
|
|
||||||
.updated(resourceType, response.data.id)
|
|
||||||
.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
@ -1,90 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.update.service', function() {
|
|
||||||
|
|
||||||
var service, $scope, $q, deferred, magnum;
|
|
||||||
var selected = {
|
|
||||||
id: 1
|
|
||||||
};
|
|
||||||
var model = {
|
|
||||||
id: 1,
|
|
||||||
tabs: "",
|
|
||||||
keypair_id: "",
|
|
||||||
coe: null
|
|
||||||
};
|
|
||||||
var modal = {
|
|
||||||
open: function(config) {
|
|
||||||
config.model = model;
|
|
||||||
deferred = $q.defer();
|
|
||||||
deferred.resolve(config);
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var workflow = {
|
|
||||||
init: function (action, title) {
|
|
||||||
action = title;
|
|
||||||
return {model: model};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////
|
|
||||||
|
|
||||||
beforeEach(module('horizon.app.core'));
|
|
||||||
beforeEach(module('horizon.framework'));
|
|
||||||
beforeEach(module('horizon.dashboard.container-infra.clusters'));
|
|
||||||
|
|
||||||
beforeEach(module(function($provide) {
|
|
||||||
$provide.value('horizon.dashboard.container-infra.clusters.workflow', workflow);
|
|
||||||
$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.update.service');
|
|
||||||
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
|
||||||
deferred = $q.defer();
|
|
||||||
deferred.resolve({data: {uuid: 1, labels: "key1:val1,key2:val2"}});
|
|
||||||
spyOn(magnum, 'getCluster').and.returnValue(deferred.promise);
|
|
||||||
spyOn(magnum, 'updateCluster').and.returnValue(deferred.promise);
|
|
||||||
spyOn(workflow, 'init').and.returnValue({model: model});
|
|
||||||
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('open the modal', inject(function($timeout) {
|
|
||||||
service.perform(selected, $scope);
|
|
||||||
|
|
||||||
expect(workflow.init).toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(modal.open).toHaveBeenCalledWith({model: model});
|
|
||||||
|
|
||||||
$timeout.flush();
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(magnum.updateCluster).toHaveBeenCalled();
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
})();
|
|
@ -0,0 +1,5 @@
|
|||||||
|
<h1 class="h4" translate>Additional Labels</h1>
|
||||||
|
|
||||||
|
<p translate>Specify additional kube_labels to apply to the cluster or override labels set by the cluster template. Overriding labels set by the cluster template may result in your cluster being misconfigured, unstable or unable to be created.</p>
|
||||||
|
|
||||||
|
<p translate>The key=value pair string is case insensitive and will be converted to lower case.</p>
|
@ -67,19 +67,60 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onGetClusterTemplate(response) {
|
function onGetClusterTemplate(response) {
|
||||||
ctrl.clusterTemplate = response.data;
|
var MODEL_DEFAULTS = $scope.model.DEFAULTS;
|
||||||
if ($scope.model.keypair === "") {
|
var template = response.data;
|
||||||
if (response.data.keypair_id === null) {
|
|
||||||
$scope.model.keypair = "";
|
ctrl.clusterTemplate = template;
|
||||||
} else {
|
|
||||||
$scope.model.keypair = response.data.keypair_id;
|
// master_lb_enabled=false? Only allow a single Master Node
|
||||||
|
$scope.model.isSingleMasterNode = template.hasOwnProperty('master_lb_enabled') &&
|
||||||
|
template.master_lb_enabled === false;
|
||||||
|
$scope.model.master_count = $scope.model.isSingleMasterNode ? 1 : $scope.model.master_count;
|
||||||
|
|
||||||
|
// Only alter the model if the value is default and exists in the response
|
||||||
|
// Warning: This is loosely coupled with default states.
|
||||||
|
// Sets response.key -> model.key
|
||||||
|
setResponseAsDefaultIfUnset('keypair_id', 'keypair');
|
||||||
|
setResponseAsDefaultIfUnset('master_count', 'master_count');
|
||||||
|
setResponseAsDefaultIfUnset('master_flavor_id', 'master_flavor_id');
|
||||||
|
setResponseAsDefaultIfUnset('node_count', 'node_count');
|
||||||
|
setResponseAsDefaultIfUnset('flavor_id', 'flavor_id');
|
||||||
|
|
||||||
|
if (template.floating_ip_enabled !== null) {
|
||||||
|
$scope.model.floating_ip_enabled = template.floating_ip_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template.labels) { return; }
|
||||||
|
|
||||||
|
$scope.model.templateLabels = template.labels;
|
||||||
|
|
||||||
|
// If a template label exists as a field on the form -> Set it as a default
|
||||||
|
setLabelResponseAsDefault('auto_scaling_enabled', 'auto_scaling_enabled', true);
|
||||||
|
setLabelResponseAsDefault('auto_healing_enabled', 'auto_healing_enabled', true);
|
||||||
|
|
||||||
|
// Set default `ingress_controller` based on its label
|
||||||
|
if (template.labels.ingress_controller !== null &&
|
||||||
|
$scope.model.ingressControllers && $scope.model.ingressControllers.length > 0) {
|
||||||
|
$scope.model.ingress_controller = MODEL_DEFAULTS.ingress_controller;
|
||||||
|
$scope.model.ingressControllers.forEach(function(controller) {
|
||||||
|
if (controller.labels && controller.labels.ingress_controller &&
|
||||||
|
controller.labels.ingress_controller === template.labels.ingress_controller) {
|
||||||
|
$scope.model.ingress_controller = controller;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setResponseAsDefaultIfUnset(responseKey, modelKey) {
|
||||||
|
if ($scope.model[modelKey] === MODEL_DEFAULTS[modelKey] &&
|
||||||
|
template[responseKey] !== null) {
|
||||||
|
$scope.model[modelKey] = template[responseKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($scope.model.docker_volume_size === "") {
|
function setLabelResponseAsDefault(labelKey, modelKey, isValueBoolean) {
|
||||||
if (response.data.docker_volume_size === null) {
|
if (template.labels[labelKey] !== null) {
|
||||||
$scope.model.docker_volume_size = "";
|
$scope.model[modelKey] = isValueBoolean
|
||||||
} else {
|
? template.labels[labelKey] === 'true'
|
||||||
$scope.model.docker_volume_size = response.data.docker_volume_size;
|
: template.labels[labelKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,42 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
describe('horizon.dashboard.container-infra.clusters', function() {
|
describe('horizon.dashboard.container-infra.clusters', function() {
|
||||||
var magnum, controller, $scope, $q, deferred;
|
var magnum, controller, $scope, $q, deferred, templateResponse, MODEL_DEFAULTS;
|
||||||
|
|
||||||
|
function getModelDefaults() {
|
||||||
|
return {
|
||||||
|
// Props used by the form
|
||||||
|
name: '',
|
||||||
|
cluster_template_id: '',
|
||||||
|
availability_zone: '',
|
||||||
|
keypair: '',
|
||||||
|
addons: [],
|
||||||
|
|
||||||
|
master_count: null,
|
||||||
|
master_flavor_id: '',
|
||||||
|
node_count: null,
|
||||||
|
flavor_id: '',
|
||||||
|
auto_scaling_enabled: false,
|
||||||
|
min_node_count: null,
|
||||||
|
max_node_count: null,
|
||||||
|
|
||||||
|
create_network: false,
|
||||||
|
fixed_network: '',
|
||||||
|
floating_ip_enabled: false,
|
||||||
|
ingress_controller: '',
|
||||||
|
|
||||||
|
auto_healing_enabled: true,
|
||||||
|
labels: '',
|
||||||
|
override_labels: false,
|
||||||
|
|
||||||
|
// Utility properties (not actively used in the form,
|
||||||
|
// populated dynamically)
|
||||||
|
id: null,
|
||||||
|
templateLabels: null,
|
||||||
|
ingressControllers: null,
|
||||||
|
isSingleMasterNode: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(module('horizon.framework'));
|
beforeEach(module('horizon.framework'));
|
||||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||||
@ -24,14 +59,63 @@
|
|||||||
beforeEach(inject(function ($injector, _$rootScope_, _$q_) {
|
beforeEach(inject(function ($injector, _$rootScope_, _$q_) {
|
||||||
$q = _$q_;
|
$q = _$q_;
|
||||||
$scope = _$rootScope_.$new();
|
$scope = _$rootScope_.$new();
|
||||||
$scope.model = {
|
$scope.model = getModelDefaults();
|
||||||
cluster_template_id: '1',
|
|
||||||
keypair: ''
|
MODEL_DEFAULTS = getModelDefaults();
|
||||||
|
// Trigger the controller's business logic
|
||||||
|
$scope.model.cluster_template_id = '1';
|
||||||
|
$scope.model.DEFAULTS = MODEL_DEFAULTS;
|
||||||
|
|
||||||
|
templateResponse = {
|
||||||
|
"coe": "kubernetes",
|
||||||
|
"docker_storage_driver": "overlay2",
|
||||||
|
"docker_volume_size": 20,
|
||||||
|
"external_network_id": "f10ad6de-a26d-4c29-8c64-2a7418d47f8f",
|
||||||
|
"fixed_network": null,
|
||||||
|
"fixed_subnet": null,
|
||||||
|
"flavor_id": "c1.c4r8",
|
||||||
|
"floating_ip_enabled": false,
|
||||||
|
"id": "6f3869a2-4cff-4e59-9e8e-ee03efa26688",
|
||||||
|
"image_id": "2beb7301-e8c8-4ac1-a321-c63e919094a9",
|
||||||
|
"insecure_registry": null,
|
||||||
|
"keypair_id": null,
|
||||||
|
"labels": {
|
||||||
|
"auto_healing_controller": "magnum-auto-healer",
|
||||||
|
"auto_healing_enabled": "true",
|
||||||
|
"auto_scaling_enabled": "false",
|
||||||
|
"cloud_provider_enabled": "true",
|
||||||
|
"cloud_provider_tag": "1.14.0-catalyst",
|
||||||
|
"container_infra_prefix": "docker.io/catalystcloud/",
|
||||||
|
"etcd_volume_size": "20",
|
||||||
|
"heat_container_agent_tag": "stein-dev",
|
||||||
|
"ingress_controller": "octavia",
|
||||||
|
"k8s_keystone_auth_tag": "v1.15.0",
|
||||||
|
"keystone_auth_enabled": "true",
|
||||||
|
"kube_dashboard_enabled": "true",
|
||||||
|
"kube_tag": "v1.15.6",
|
||||||
|
"magnum_auto_healer_tag": "v1.15.0-catalyst.0",
|
||||||
|
"master_lb_floating_ip_enabled": "false",
|
||||||
|
"octavia_ingress_controller_tag": "1.14.0-catalyst",
|
||||||
|
"prometheus_monitoring": "true"
|
||||||
|
},
|
||||||
|
"master_flavor_id": "c1.c2r4",
|
||||||
|
"master_lb_enabled": true,
|
||||||
|
"name": "kubernetes-v1.15.6-prod-20191129",
|
||||||
|
"network_driver": "calico",
|
||||||
|
"no_proxy": null,
|
||||||
|
"project_id": "94b566de52f9423fab80ceee8c0a4a23",
|
||||||
|
"public": true,
|
||||||
|
"registry_enabled": false,
|
||||||
|
"server_type": "vm",
|
||||||
|
"tls_disabled": false,
|
||||||
|
"user_id": "098b4de3d94649f8b9ae5bf5ee59451c",
|
||||||
|
"volume_driver": "cinder"
|
||||||
};
|
};
|
||||||
|
|
||||||
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||||
controller = $injector.get('$controller');
|
controller = $injector.get('$controller');
|
||||||
deferred = $q.defer();
|
deferred = $q.defer();
|
||||||
deferred.resolve({data: {keypair_id: '1'}});
|
deferred.resolve({data: templateResponse});
|
||||||
spyOn(magnum, 'getClusterTemplate').and.returnValue(deferred.promise);
|
spyOn(magnum, 'getClusterTemplate').and.returnValue(deferred.promise);
|
||||||
createController($scope);
|
createController($scope);
|
||||||
}));
|
}));
|
||||||
@ -49,14 +133,127 @@
|
|||||||
expect(magnum.getClusterTemplate).toHaveBeenCalled();
|
expect(magnum.getClusterTemplate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keypair is changed by cluster template\'s keypair', function() {
|
it('should override some model default properties by values from ' +
|
||||||
$scope.model.cluster_template_id = '1';
|
'retrieved cluster template', function() {
|
||||||
$scope.$apply();
|
templateResponse.keypair_id = 1;
|
||||||
expect($scope.model.keypair).toBe('1');
|
templateResponse.master_count = 1;
|
||||||
|
templateResponse.master_flavor_id = 'ABC';
|
||||||
|
templateResponse.node_count = 1;
|
||||||
|
templateResponse.flavor_id = 'ABC';
|
||||||
|
|
||||||
$scope.model.cluster_template_id = '';
|
var model = $scope.model;
|
||||||
|
model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
expect($scope.model.keypair).toBe('');
|
|
||||||
|
expect(model.keypair).toBe(1);
|
||||||
|
expect(model.master_count).toBe(1);
|
||||||
|
expect(model.master_flavor_id).toEqual('ABC');
|
||||||
|
expect(model.node_count).toBe(1);
|
||||||
|
expect(model.flavor_id).toEqual('ABC');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override some non-default model properties by values ' +
|
||||||
|
'from retrieved cluster template', function() {
|
||||||
|
var model = $scope.model;
|
||||||
|
|
||||||
|
model.keypair = 99;
|
||||||
|
model.master_count = 99;
|
||||||
|
model.master_flavor_id = 'XYZ';
|
||||||
|
model.node_count = 99;
|
||||||
|
model.flavor_id = 'XYZ';
|
||||||
|
|
||||||
|
templateResponse.keypair_id = 1;
|
||||||
|
templateResponse.master_count = 1;
|
||||||
|
templateResponse.master_flavor_id = 'ABC';
|
||||||
|
templateResponse.node_count = 1;
|
||||||
|
templateResponse.flavor_id = 'ABC';
|
||||||
|
|
||||||
|
model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
expect(model.keypair).toBe(99);
|
||||||
|
expect(model.master_count).toBe(99);
|
||||||
|
expect(model.master_flavor_id).toEqual('XYZ');
|
||||||
|
expect(model.node_count).toBe(99);
|
||||||
|
expect(model.flavor_id).toEqual('XYZ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set number of Master Nodes to 1 if the cluster template ' +
|
||||||
|
'response contains negative `master_lb_enabled` flag', function() {
|
||||||
|
$scope.model.master_count = 99;
|
||||||
|
templateResponse.master_lb_enabled = false;
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
expect($scope.model.master_count).toBe(1);
|
||||||
|
|
||||||
|
$scope.model.master_count = MODEL_DEFAULTS.master_count;
|
||||||
|
$scope.model.cluster_template_id = '999'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
expect($scope.model.master_count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not process labels if they are not available in the cluster ' +
|
||||||
|
'template response', function() {
|
||||||
|
templateResponse.labels = null;
|
||||||
|
$scope.model.labels = MODEL_DEFAULTS.labels;
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
expect($scope.model.labels).toEqual(MODEL_DEFAULTS.labels);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always override some model properties by values from ' +
|
||||||
|
'retrieved cluster template', function() {
|
||||||
|
$scope.model.floating_ip_enabled = !MODEL_DEFAULTS.floating_ip_enabled;
|
||||||
|
templateResponse.floating_ip_enabled = !$scope.model.floating_ip_enabled;
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
expect($scope.model.floating_ip_enabled).toBe(templateResponse.floating_ip_enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always override some model\'s properties by values from ' +
|
||||||
|
'retrieved cluster template\'s labels', function() {
|
||||||
|
var model = $scope.model;
|
||||||
|
|
||||||
|
model.auto_scaling_enabled = true;
|
||||||
|
templateResponse.labels.auto_scaling_enabled = 'true';
|
||||||
|
model.auto_healing_enabled = true;
|
||||||
|
templateResponse.labels.auto_healing_enabled = 'false';
|
||||||
|
model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
expect(model.auto_scaling_enabled).toBe(true);
|
||||||
|
expect(model.auto_healing_enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail if the cluster template response is empty', function() {
|
||||||
|
templateResponse = {};
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail if the cluster template\'s labels are empty', function() {
|
||||||
|
templateResponse = {labels:{}};
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the correct Ingress Controller on the model based on the ' +
|
||||||
|
'label in cluster template response', function() {
|
||||||
|
// Controllers retrieved from the API
|
||||||
|
$scope.model.ingressControllers = [
|
||||||
|
{ name: 'Controller1', labels: { ingress_controller: 'c1'}},
|
||||||
|
{ name: 'Controller2', labels: { ingress_controller: 'c2'}},
|
||||||
|
{ name: 'Controller3', labels: { ingress_controller: 'c3'}},
|
||||||
|
];
|
||||||
|
|
||||||
|
templateResponse.labels.ingress_controller = 'c2';
|
||||||
|
|
||||||
|
$scope.model.cluster_template_id = '99'; // Triggers bussines logic revalidation
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
expect($scope.model.ingress_controller.labels.ingress_controller).toBe('c2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
<h1 class="h4" translate>Addon software</h1>
|
||||||
|
<p translate>Any addon software selected will be installed at the latest supported version when the cluster is deployed, but will not be upgraded automatically by rolling upgrades.</p>
|
@ -1 +0,0 @@
|
|||||||
<p translate>Specify cluster name and choose cluster template</p>
|
|
@ -1 +0,0 @@
|
|||||||
<p translate>Arbitrary labels in the form of key=value pairs to associate with a clusters. May be used multiple times.</p>
|
|
@ -0,0 +1,2 @@
|
|||||||
|
<h1 class="h4" translate>Security Updates</h1>
|
||||||
|
<p translate>Please note that updates may cause application downtime if workloads deployed to Kubernetes are not following the best practices outlined in the documentation (for example, not using multiple replicas).</p>
|
@ -1,9 +0,0 @@
|
|||||||
<p translate>Specify conditions for cluster creation.</p>
|
|
||||||
<dl>
|
|
||||||
<dt>Keypair</dt>
|
|
||||||
<dd>
|
|
||||||
When the selected cluster template contains keypair, user can either provide a new keypair
|
|
||||||
for the cluster or inherit one from the cluster template. When the selected cluster template
|
|
||||||
has no keypair attached, user has to provide a keypair for the cluster.
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
@ -0,0 +1,4 @@
|
|||||||
|
<h1 class="h4" translate>Cluster API</h1>
|
||||||
|
|
||||||
|
<p translate>Making the Kubernetes API accessible from the private network only is the most secure option (the default), but access will be limited to compute instances on the same private network or a VPN to that network.</p>
|
||||||
|
<p translate>Making the Kubernetes API accessible from anywhere on the public internet is convenient, but may represent a security risk. <em>[When selecting this option, it is recommended to limit access to a trusted IP address range.]</em></p>
|
@ -1 +1,6 @@
|
|||||||
<p translate>Specify the number of master nodes, cluster nodes and docker volume size for the cluster.</p>
|
<h1 class="h4" translate>Auto Scaling</h1>
|
||||||
|
|
||||||
|
<p translate>If enabled, the minimum and maximum number of worker nodes must be specified.</p>
|
||||||
|
<p translate>Auto scaling requires the use of CPU and memory limits on the resource definition of Pods.</p>
|
||||||
|
<p translate>If Kubernetes is unable to schedule a Pod due to insuficient CPU or memory in the cluster, a worker node will be added, as long as the maximum number of worker nodes has not been reached.</p>
|
||||||
|
<p translate>If the aggregate resource limits of all existing Pods is lower than 50% of the cluster capacity, a worker node will be removed, as long as the minimum number of worker nodes has not been reached.</p>
|
||||||
|
@ -17,6 +17,15 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name horizon.dashboard.container-infra.clusters.workflow
|
||||||
|
* @ngModule
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Provides business logic for Cluster creation workflow, including data model,
|
||||||
|
* UI form schema and configuration, fetching and processing of required data.
|
||||||
|
*/
|
||||||
angular
|
angular
|
||||||
.module('horizon.dashboard.container-infra.clusters')
|
.module('horizon.dashboard.container-infra.clusters')
|
||||||
.factory(
|
.factory(
|
||||||
@ -24,225 +33,424 @@
|
|||||||
ClusterWorkflow);
|
ClusterWorkflow);
|
||||||
|
|
||||||
ClusterWorkflow.$inject = [
|
ClusterWorkflow.$inject = [
|
||||||
|
'$q',
|
||||||
'horizon.dashboard.container-infra.basePath',
|
'horizon.dashboard.container-infra.basePath',
|
||||||
'horizon.app.core.workflow.factory',
|
|
||||||
'horizon.framework.util.i18n.gettext',
|
'horizon.framework.util.i18n.gettext',
|
||||||
'horizon.app.core.openstack-service-api.magnum',
|
'horizon.app.core.openstack-service-api.magnum',
|
||||||
|
'horizon.app.core.openstack-service-api.neutron',
|
||||||
'horizon.app.core.openstack-service-api.nova'
|
'horizon.app.core.openstack-service-api.nova'
|
||||||
];
|
];
|
||||||
|
|
||||||
function ClusterWorkflow(basePath, workflowService, gettext, magnum, nova) {
|
// comma-separated key=value with optional space after comma
|
||||||
|
var REGEXP_KEY_VALUE = /^(\w+=[^,]+,?\s?)+$/;
|
||||||
|
|
||||||
|
function ClusterWorkflow($q, basePath, gettext, magnum, neutron, nova) {
|
||||||
var workflow = {
|
var workflow = {
|
||||||
init: init
|
init: init
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(action, title, $scope) {
|
function init(title, $scope) {
|
||||||
var schema, form, model, nflavors, mflavors;
|
var schema, form;
|
||||||
var clusterTemplates = [{value:"", name: gettext("Choose a Cluster Template")}];
|
|
||||||
var keypairs = [{value:"", name: gettext("Choose a Keypair")}];
|
// Default <option>s; will be shown in selector as a placeholder
|
||||||
var dockerVolumeSizeDescription = gettext(
|
var templateTitleMap = [{value: '', name: gettext('Choose a Cluster Template') }];
|
||||||
"If not specified, the value specified in Cluster Template will be used.");
|
var availabilityZoneTitleMap = [{value: '',
|
||||||
|
name: gettext('Choose an Availability Zone')}];
|
||||||
|
var keypairsTitleMap = [{value: '', name: gettext('Choose a Keypair')}];
|
||||||
|
var masterFlavorTitleMap = [{value: '',
|
||||||
|
name: gettext('Choose a Flavor for the Master Node')}];
|
||||||
|
var workerFlavorTitleMap = [{value: '',
|
||||||
|
name: gettext('Choose a Flavor for the Worker Node')}];
|
||||||
|
var networkTitleMap = [{value: '', name: gettext('Choose an existing network')}];
|
||||||
|
var ingressTitleMap = [{value: '', name: gettext('Choose an ingress controller')}];
|
||||||
|
|
||||||
|
var addonsTitleMap = [];
|
||||||
|
|
||||||
|
var MODEL_DEFAULTS = getModelDefaults();
|
||||||
|
var model = getModelDefaults();
|
||||||
|
|
||||||
// schema
|
|
||||||
schema = {
|
schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
'name': {
|
'name': { type: 'string' },
|
||||||
title: gettext('Cluster Name'),
|
'cluster_template_id': { type: 'string' },
|
||||||
type: 'string'
|
'availability_zone': { type: 'string' },
|
||||||
},
|
'keypair': { type: 'string' },
|
||||||
'cluster_template_id': {
|
'addons': {
|
||||||
title: gettext('Cluster Template'),
|
type: 'array',
|
||||||
type: 'string'
|
items: { type: 'object' },
|
||||||
|
minItems: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
'master_count': {
|
'master_count': {
|
||||||
title: gettext('Master Count'),
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1
|
minimum: 1
|
||||||
},
|
},
|
||||||
|
'master_flavor_id': { type: 'string' },
|
||||||
'node_count': {
|
'node_count': {
|
||||||
title: gettext('Node Count'),
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1
|
minimum: 1
|
||||||
},
|
},
|
||||||
'discovery_url': {
|
'flavor_id': { type: 'string' },
|
||||||
title: gettext('Discovery URL'),
|
'auto_scaling_enabled': { type: 'boolean' },
|
||||||
type: 'string'
|
'min_node_count': {
|
||||||
},
|
|
||||||
'create_timeout': {
|
|
||||||
title: gettext('Timeout'),
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 0
|
minimum: 1
|
||||||
},
|
},
|
||||||
'keypair': {
|
'max_node_count': { type: 'number' },
|
||||||
title: gettext('Keypair'),
|
|
||||||
type: 'string'
|
'create_network': { type: 'boolean' },
|
||||||
},
|
'fixed_network': { type: 'string' },
|
||||||
'docker_volume_size': {
|
'floating_ip_enabled': { type: 'boolean' },
|
||||||
title: gettext('Docker Volume Size (GB)'),
|
'ingress_controller': { type: 'object' },
|
||||||
type: 'number'
|
|
||||||
},
|
'auto_healing_enabled': { type: 'boolean' },
|
||||||
'master_flavor_id': {
|
|
||||||
title: gettext('Master Flavor ID'),
|
'labels': { type: 'string' },
|
||||||
type: 'string'
|
'override_labels': { type: 'boolean' }
|
||||||
},
|
|
||||||
'flavor_id': {
|
|
||||||
title: gettext('Node Flavor ID'),
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
'rollback': {
|
|
||||||
title: gettext('Rollback cluster on update failure'),
|
|
||||||
type: 'boolean'
|
|
||||||
},
|
|
||||||
'labels': {
|
|
||||||
title: gettext('Labels'),
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// form
|
var formMasterCount = {
|
||||||
|
key: 'master_count',
|
||||||
|
title: gettext('Number of Master Nodes'),
|
||||||
|
placeholder: gettext('The number of master nodes for the cluster'),
|
||||||
|
required: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disable the Master Count field, if only a single master is allowed
|
||||||
|
var isSingleMasterNodeWatcher = $scope.$watch(
|
||||||
|
function() { return model.isSingleMasterNode; },
|
||||||
|
function(isSingle) {
|
||||||
|
if (typeof isSingle !== 'undefined') {
|
||||||
|
formMasterCount.readonly = isSingle;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true);
|
||||||
|
|
||||||
form = [
|
form = [
|
||||||
{
|
{
|
||||||
type:'tabs',
|
type:'tabs',
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
title: gettext('Info'),
|
title: gettext('Details'),
|
||||||
help: basePath + 'clusters/workflow/info.help.html',
|
help: basePath + 'clusters/workflow/details.help.html',
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'row',
|
htmlClass: 'row',
|
||||||
|
required: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'col-xs-12',
|
htmlClass: 'col-md-8',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
placeholder: gettext('Name of the cluster.'),
|
title: gettext('Cluster Name'),
|
||||||
readonly: action === 'update'
|
placeholder: gettext('Name of the cluster'),
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cluster_template_id',
|
key: 'cluster_template_id',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
titleMap: clusterTemplates,
|
title: gettext('Cluster Template'),
|
||||||
required: true,
|
titleMap: templateTitleMap,
|
||||||
readonly: action === 'update'
|
required: true
|
||||||
},
|
},
|
||||||
|
// Details of the chosen Cluster Template
|
||||||
{
|
{
|
||||||
type: 'template',
|
type: 'template',
|
||||||
templateUrl: basePath + 'clusters/workflow/cluster-template.html'
|
templateUrl: basePath + 'clusters/workflow/cluster-template.html'
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: gettext('Size'),
|
|
||||||
help: basePath + 'clusters/workflow/size.help.html',
|
|
||||||
type: 'section',
|
|
||||||
htmlClass: 'row',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'section',
|
|
||||||
htmlClass: 'col-xs-12',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: 'master_count',
|
|
||||||
placeholder: gettext('The number of master nodes for the cluster.'),
|
|
||||||
readonly: action === 'update'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'node_count',
|
key: 'availability_zone',
|
||||||
placeholder: gettext('The cluster node count.')
|
type: 'select',
|
||||||
|
title: gettext('Availability Zone'),
|
||||||
|
titleMap: availabilityZoneTitleMap,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'docker_volume_size',
|
key: 'keypair',
|
||||||
placeholder: gettext('Specify the size in GB for the docker volume'),
|
type: 'select',
|
||||||
description: dockerVolumeSizeDescription,
|
title: gettext('Keypair'),
|
||||||
readonly: action === 'update'
|
titleMap: keypairsTitleMap,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rollback',
|
key: 'addons',
|
||||||
condition: action === 'create'
|
type: 'checkboxes',
|
||||||
|
title: gettext('Addon Software'),
|
||||||
|
disableSuccessState: true,
|
||||||
|
titleMap: addonsTitleMap
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: gettext('Misc'),
|
title: gettext('Size'),
|
||||||
help: basePath + 'clusters/workflow/misc.help.html',
|
help: basePath + 'clusters/workflow/size.help.html',
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'row',
|
htmlClass: 'row',
|
||||||
|
required: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'col-xs-12',
|
htmlClass: 'col-md-8',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'discovery_url',
|
type: 'fieldset',
|
||||||
placeholder: gettext('Specifies custom discovery url for node discovery.'),
|
title: gettext('Master Nodes'),
|
||||||
readonly: action === 'update'
|
items: [
|
||||||
|
formMasterCount,
|
||||||
|
// Info message explaining why only single master node is enabled
|
||||||
|
{
|
||||||
|
type: 'template',
|
||||||
|
template: '<div class="alert alert-info">' +
|
||||||
|
'<span class="fa fa-info-circle"></span> ' +
|
||||||
|
gettext('The selected Cluster Template does not support ' +
|
||||||
|
'multiple master nodes.') +
|
||||||
|
'</div>',
|
||||||
|
condition: 'model.isSingleMasterNode == true'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'master_flavor_id',
|
||||||
|
title: gettext('Flavor of Master Nodes'),
|
||||||
|
type: 'select',
|
||||||
|
titleMap: masterFlavorTitleMap,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create_timeout',
|
type: 'fieldset',
|
||||||
/* eslint-disable max-len */
|
title: gettext('Worker Nodes'),
|
||||||
placeholder: gettext('The timeout for cluster creation in minutes.'),
|
items: [
|
||||||
description: gettext('Set to 0 for no timeout. The default is no timeout.'),
|
{
|
||||||
readonly: action === 'update'
|
key: 'node_count',
|
||||||
|
title: gettext('Number of Worker Nodes'),
|
||||||
|
placeholder: gettext('The number of worker nodes for the cluster'),
|
||||||
|
required: true,
|
||||||
|
onChange: autosetScalingModelValues
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'flavor_id',
|
||||||
|
title: gettext('Flavor of Worker Nodes'),
|
||||||
|
type: 'select',
|
||||||
|
titleMap: workerFlavorTitleMap,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'keypair',
|
type: 'fieldset',
|
||||||
type: 'select',
|
title: gettext('Auto Scaling'),
|
||||||
titleMap: keypairs,
|
items: [
|
||||||
required: true,
|
{
|
||||||
readonly: action === 'update'
|
key: 'auto_scaling_enabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
title: gettext('Auto-scale Worker Nodes'),
|
||||||
|
onChange: function(isAutoScaling) {
|
||||||
|
// Reset dependant model fields to defaults first
|
||||||
|
model.min_node_count = MODEL_DEFAULTS.min_node_count;
|
||||||
|
model.max_node_count = MODEL_DEFAULTS.max_node_count;
|
||||||
|
|
||||||
|
if (isAutoScaling) { autosetScalingModelValues(); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'min_node_count',
|
||||||
|
title: gettext('Minimum Number of Worker Nodes'),
|
||||||
|
placeholder: gettext('Minimum Number of Worker Nodes'),
|
||||||
|
validationMessage: {
|
||||||
|
101: gettext('You cannot auto-scale to less than ' +
|
||||||
|
'a single Worker Node.'),
|
||||||
|
103: gettext('The minimum number of Worker Nodes a ' +
|
||||||
|
'new cluster can auto scale to cannot exceed the ' +
|
||||||
|
'total amount of Worker Nodes.'),
|
||||||
|
maximumExceeded: gettext('A minimum number of Worker ' +
|
||||||
|
'Nodes cannot be higher than the default number of Worker Nodes.')
|
||||||
|
},
|
||||||
|
$validators: {
|
||||||
|
maximumExceeded: function(minNodeCount) {
|
||||||
|
return !model.node_count || minNodeCount <= model.node_count;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
condition: 'model.auto_scaling_enabled === true',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max_node_count',
|
||||||
|
title: gettext('Maximum number of Worker Nodes'),
|
||||||
|
placeholder: gettext('Maximum number of Worker Nodes'),
|
||||||
|
validationMessage: {
|
||||||
|
101: gettext('The maximum number of Worker Nodes a new cluster ' +
|
||||||
|
'can auto-scale to cannot be less than the total amount of ' +
|
||||||
|
'Worker Nodes.'),
|
||||||
|
minimumExceeded: gettext('The maximum number of Worker Nodes cannot ' +
|
||||||
|
'be less than the default number of Worker Nodes and 1.')
|
||||||
|
},
|
||||||
|
$validators: {
|
||||||
|
minimumExceeded: function(maxNodeCount) {
|
||||||
|
return maxNodeCount > 0 && (!model.node_count ||
|
||||||
|
maxNodeCount >= model.node_count);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
condition: 'model.auto_scaling_enabled === true',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: gettext('Network'),
|
||||||
|
help: basePath + 'clusters/workflow/network.help.html',
|
||||||
|
type: 'section',
|
||||||
|
htmlClass: 'row',
|
||||||
|
required: true,
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'col-xs-6',
|
htmlClass: 'col-md-8',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'master_flavor_id',
|
type: 'fieldset',
|
||||||
type: 'select',
|
title: gettext('Network'),
|
||||||
titleMap: mflavors,
|
items: [
|
||||||
readonly: action === 'update'
|
{
|
||||||
}
|
key: 'create_network',
|
||||||
]
|
title: gettext('Create New Network'),
|
||||||
},
|
onChange: function(isNewNetwork) {
|
||||||
{
|
if (isNewNetwork) {
|
||||||
type: 'section',
|
model.fixed_network = MODEL_DEFAULTS.fixed_network;
|
||||||
htmlClass: 'col-xs-6',
|
}
|
||||||
items: [
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fixed_network',
|
||||||
|
type: 'select',
|
||||||
|
title: gettext('Use an Existing Network'),
|
||||||
|
titleMap: networkTitleMap,
|
||||||
|
condition: 'model.create_network === false',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'flavor_id',
|
type: 'fieldset',
|
||||||
type: 'select',
|
title: gettext('Network Access Control'),
|
||||||
titleMap: nflavors,
|
items: [
|
||||||
readonly: action === 'update'
|
{
|
||||||
|
key: 'floating_ip_enabled',
|
||||||
|
type: 'select',
|
||||||
|
title: gettext('Cluster API'),
|
||||||
|
titleMap: [
|
||||||
|
{value: false, name: gettext('Accessible on private network only')},
|
||||||
|
{value: true, name: gettext('Accessible on the public internet')}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Warning message for the Cluster API
|
||||||
|
{
|
||||||
|
type: 'template',
|
||||||
|
template: '<div class="alert alert-warning">' +
|
||||||
|
'<span class="fa fa-warning"></span> ' +
|
||||||
|
gettext('It is generally not recommended to give public access.') +
|
||||||
|
'</div>',
|
||||||
|
condition: 'model.floating_ip_enabled == true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'fieldset',
|
||||||
|
title: gettext('Ingress'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'ingress_controller',
|
||||||
|
title: gettext('Ingress Controller'),
|
||||||
|
type: 'select',
|
||||||
|
titleMap: ingressTitleMap
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: gettext('Labels'),
|
title: gettext('Management'),
|
||||||
help: basePath + 'clusters/workflow/labels.help.html',
|
help: basePath + 'clusters/workflow/management.help.html',
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'row',
|
htmlClass: 'row',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
htmlClass: 'col-xs-12',
|
htmlClass: 'col-md-8',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'labels',
|
type: 'fieldset',
|
||||||
type: 'textarea',
|
title: gettext('Auto Healing'),
|
||||||
placeholder: gettext('KEY1=VALUE1, KEY2=VALUE2...'),
|
items: [
|
||||||
readonly: action === 'update'
|
{
|
||||||
|
key: 'auto_healing_enabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
title: gettext('Automatically Repair Unhealthy Nodes')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: gettext('Advanced'),
|
||||||
|
help: basePath + 'clusters/workflow/advanced.help.html',
|
||||||
|
type: 'section',
|
||||||
|
htmlClass: 'row',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'section',
|
||||||
|
htmlClass: 'col-md-8',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'fieldset',
|
||||||
|
title: gettext('Labels'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'labels',
|
||||||
|
type: 'textarea',
|
||||||
|
title: gettext('Additional Labels'),
|
||||||
|
placeholder: gettext('key=value,key2=value2...'),
|
||||||
|
validationMessage: {
|
||||||
|
invalidFormat: gettext('Invalid format. Must be a comma-separated ' +
|
||||||
|
'key-value string: key=value,key2=value2')
|
||||||
|
},
|
||||||
|
$validators: {
|
||||||
|
invalidFormat: function(labelsString) {
|
||||||
|
return labelsString === '' || REGEXP_KEY_VALUE.test(labelsString);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disableSuccessState: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'override_labels',
|
||||||
|
type: 'checkbox',
|
||||||
|
title: gettext('I do want to override Template and Workflow Labels'),
|
||||||
|
condition: 'model.labels !== ""',
|
||||||
|
},
|
||||||
|
// Warning message for the label override
|
||||||
|
{
|
||||||
|
type: 'template',
|
||||||
|
template: '<div class="alert alert-warning">' +
|
||||||
|
'<span class="fa fa-warning"></span> ' +
|
||||||
|
gettext('Overriding labels already defined by cluster template or' +
|
||||||
|
'workflow might result in unpredictable behaviour.') +
|
||||||
|
'</div>',
|
||||||
|
condition: 'model.override_labels == true'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -252,58 +460,160 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
magnum.getClusterTemplates().then(onGetClusterTemplates);
|
function getModelDefaults() {
|
||||||
nova.getKeypairs().then(onGetKeypairs);
|
return {
|
||||||
nova.getFlavors(false, false).then(onGetFlavors);
|
// Props used by the form
|
||||||
|
name: '',
|
||||||
|
cluster_template_id: '',
|
||||||
|
availability_zone: '',
|
||||||
|
keypair: '',
|
||||||
|
addons: [],
|
||||||
|
|
||||||
|
master_count: null,
|
||||||
|
master_flavor_id: '',
|
||||||
|
node_count: null,
|
||||||
|
flavor_id: '',
|
||||||
|
auto_scaling_enabled: false,
|
||||||
|
min_node_count: null,
|
||||||
|
max_node_count: null,
|
||||||
|
|
||||||
|
create_network: true,
|
||||||
|
fixed_network: '',
|
||||||
|
floating_ip_enabled: false,
|
||||||
|
ingress_controller: '',
|
||||||
|
|
||||||
|
auto_healing_enabled: true,
|
||||||
|
labels: '',
|
||||||
|
override_labels: false,
|
||||||
|
|
||||||
|
// Utility properties (not actively used in the form,
|
||||||
|
// populated dynamically)
|
||||||
|
id: null,
|
||||||
|
templateLabels: null,
|
||||||
|
ingressControllers: null,
|
||||||
|
isSingleMasterNode: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function autosetScalingModelValues() {
|
||||||
|
var nodeCount = model.node_count;
|
||||||
|
if (nodeCount && nodeCount > 0 && model.auto_scaling_enabled) {
|
||||||
|
|
||||||
|
// Set defaults to related modal fields (have they not been changed)
|
||||||
|
if (model.min_node_count === MODEL_DEFAULTS.min_node_count) {
|
||||||
|
model.min_node_count = nodeCount > 1 ? nodeCount - 1 : 1;
|
||||||
|
} else if (nodeCount < model.min_node_count) {
|
||||||
|
model.min_node_count = nodeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.max_node_count === MODEL_DEFAULTS.max_node_count) {
|
||||||
|
model.max_node_count = nodeCount + 1;
|
||||||
|
} else if (nodeCount > model.max_node_count) {
|
||||||
|
model.max_node_count = nodeCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onGetKeypairs(response) {
|
function onGetKeypairs(response) {
|
||||||
angular.forEach(response.data.items, function(item) {
|
var items = response.data.items;
|
||||||
keypairs.push({value: item.keypair.name, name: item.keypair.name});
|
|
||||||
|
angular.forEach(items, function(item) {
|
||||||
|
keypairsTitleMap.push({
|
||||||
|
value: item.keypair.name,
|
||||||
|
name: item.keypair.name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length === 1) {
|
||||||
|
model.keypair = items[0].keypair.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetAvailabilityZones(response) {
|
||||||
|
angular.forEach(response.data.items, function(availabilityZone) {
|
||||||
|
availabilityZoneTitleMap.push({
|
||||||
|
value: availabilityZone.zoneName,
|
||||||
|
name: availabilityZone.zoneName
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setSingleItemAsDefault(response.data.items, 'availability_zone', 'zoneName');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetAddons(response) {
|
||||||
|
angular.forEach(response.data.addons, function(addon) {
|
||||||
|
addonsTitleMap.push({ value: addon, name: addon.name });
|
||||||
|
// Pre-selected by default
|
||||||
|
if (addon.selected) { model.addons.push(addon); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGetFlavors(response) {
|
function onGetFlavors(response) {
|
||||||
nflavors = [{value:"", name: gettext("Choose a Flavor for the Node")}];
|
angular.forEach(response.data.items, function(flavor) {
|
||||||
mflavors = [{value:"", name: gettext("Choose a Flavor for the Master Node")}];
|
workerFlavorTitleMap.push({value: flavor.name, name: flavor.name});
|
||||||
angular.forEach(response.data.items, function(item) {
|
masterFlavorTitleMap.push({value: flavor.name, name: flavor.name});
|
||||||
nflavors.push({value: item.name, name: item.name});
|
|
||||||
mflavors.push({value: item.name, name: item.name});
|
|
||||||
});
|
});
|
||||||
form[0].tabs[2].items[1].items[0].titleMap = mflavors;
|
|
||||||
form[0].tabs[2].items[2].items[0].titleMap = nflavors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGetClusterTemplates(response) {
|
function onGetClusterTemplates(response) {
|
||||||
angular.forEach(response.data.items, function(item) {
|
angular.forEach(response.data.items, function(clusterTemplate) {
|
||||||
clusterTemplates.push({value: item.id, name: item.name});
|
templateTitleMap.push({value: clusterTemplate.id, name: clusterTemplate.name});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
model = {
|
function onGetNetworks(response) {
|
||||||
name: "",
|
angular.forEach(response.data.items, function(network) {
|
||||||
cluster_template_id: "",
|
networkTitleMap.push({value: network.id, name: network.name + ' (' + network.id + ')'});
|
||||||
master_count: null,
|
});
|
||||||
node_count: null,
|
|
||||||
docker_volume_size: "",
|
|
||||||
rollback: false,
|
|
||||||
discovery_url: "",
|
|
||||||
create_timeout: null,
|
|
||||||
keypair: "",
|
|
||||||
flavor_id: "",
|
|
||||||
master_flavor_id: "",
|
|
||||||
labels: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = {
|
setSingleItemAsDefault(response.data.items, 'fixed_network', 'id');
|
||||||
title: title,
|
}
|
||||||
schema: schema,
|
|
||||||
form: form,
|
|
||||||
model: model
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.model = model;
|
function onGetIngressControllers(response) {
|
||||||
|
angular.forEach(response.data.controllers, function(ingressController) {
|
||||||
|
ingressTitleMap.push({value: ingressController, name: ingressController.name});
|
||||||
|
});
|
||||||
|
|
||||||
return config;
|
model.ingressControllers = response.data.controllers;
|
||||||
|
|
||||||
|
// Set first item to defaults
|
||||||
|
if (model.ingressControllers.length > 0) {
|
||||||
|
model.ingress_controller = ingressTitleMap[1].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSingleItemAsDefault(itemsList, modelKey, itemKey) {
|
||||||
|
if (itemsList.length === 1) {
|
||||||
|
model[modelKey] = itemsList[0][itemKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
isSingleMasterNodeWatcher();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all the dependencies from APIs and return Promise
|
||||||
|
// with a form configuration object.
|
||||||
|
return $q.all([
|
||||||
|
magnum.getClusterTemplates().then(onGetClusterTemplates),
|
||||||
|
nova.getAvailabilityZones().then(onGetAvailabilityZones),
|
||||||
|
nova.getKeypairs().then(onGetKeypairs),
|
||||||
|
neutron.getNetworks().then(onGetNetworks),
|
||||||
|
magnum.getAddons().then(onGetAddons),
|
||||||
|
nova.getFlavors(false, false).then(onGetFlavors),
|
||||||
|
magnum.getIngressControllers().then(onGetIngressControllers)
|
||||||
|
]).then(function() {
|
||||||
|
$scope.model = model;
|
||||||
|
$scope.model.DEFAULTS = MODEL_DEFAULTS;
|
||||||
|
|
||||||
|
// Modal Config
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
schema: schema,
|
||||||
|
form: form,
|
||||||
|
model: model
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('horizon.dashboard.container-infra.clusters.workflow', function() {
|
describe('horizon.dashboard.container-infra.clusters.workflow', function() {
|
||||||
|
var workflow, magnum, nova, neutron, $scope, $q, deferred, keyDeferred, controllersDeferred,
|
||||||
var workflow, magnum, nova, $scope, $q, deferred, keyDeferred;
|
controllersResponse, networkDeferred, zoneDeferred, addonsResponse, addonDeferred;
|
||||||
|
|
||||||
beforeEach(module('horizon.app.core'));
|
beforeEach(module('horizon.app.core'));
|
||||||
beforeEach(module('horizon.framework'));
|
beforeEach(module('horizon.framework'));
|
||||||
@ -28,26 +28,74 @@
|
|||||||
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
|
||||||
$q = _$q_;
|
$q = _$q_;
|
||||||
$scope = _$rootScope_.$new();
|
$scope = _$rootScope_.$new();
|
||||||
|
|
||||||
workflow = $injector.get(
|
workflow = $injector.get(
|
||||||
'horizon.dashboard.container-infra.clusters.workflow');
|
'horizon.dashboard.container-infra.clusters.workflow');
|
||||||
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||||
|
neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
|
||||||
nova = $injector.get('horizon.app.core.openstack-service-api.nova');
|
nova = $injector.get('horizon.app.core.openstack-service-api.nova');
|
||||||
|
|
||||||
deferred = $q.defer();
|
deferred = $q.defer();
|
||||||
deferred.resolve({data:{items:{1:{name:1},2:{name:2}}}});
|
deferred.resolve({data:{items:{1:{name:1},2:{name:2}}}});
|
||||||
|
|
||||||
keyDeferred = $q.defer();
|
keyDeferred = $q.defer();
|
||||||
keyDeferred.resolve({data:{items:{1:{keypair:{name:1}},2:{keypair:{name:2}}}}});
|
keyDeferred.resolve({data:{items:{1:{keypair:{name:1}},2:{keypair:{name:2}}}}});
|
||||||
|
|
||||||
|
controllersResponse = {controllers:[
|
||||||
|
{name: 'Controller1', labels:{ingress_controller:'ic1'}},
|
||||||
|
{name: 'Controller2', labels:{ingress_controller:'ic2'}},
|
||||||
|
{name: 'Controller3', labels:{ingress_controller:'ic3'}},
|
||||||
|
]};
|
||||||
|
controllersDeferred = $q.defer();
|
||||||
|
controllersDeferred.resolve({data: controllersResponse});
|
||||||
|
|
||||||
|
networkDeferred = $q.defer();
|
||||||
|
networkDeferred.resolve({data:{items:[
|
||||||
|
{id: '1', name: 'Network1'},
|
||||||
|
{id: '2', name: 'Network2'}
|
||||||
|
]}});
|
||||||
|
|
||||||
|
zoneDeferred = $q.defer();
|
||||||
|
zoneDeferred.resolve({data:{items:[
|
||||||
|
{zoneName: 'zone1'},
|
||||||
|
{zoneName: 'zone2'}
|
||||||
|
]}});
|
||||||
|
|
||||||
|
addonsResponse = {addons:[
|
||||||
|
{name: 'Addon1', labels:{}, selected: false},
|
||||||
|
{name: 'Addon2', labels:{}, selected: true},
|
||||||
|
{name: 'Addon3', labels:{}, selected: true},
|
||||||
|
]};
|
||||||
|
addonDeferred = $q.defer();
|
||||||
|
addonDeferred.resolve({data: addonsResponse});
|
||||||
|
|
||||||
spyOn(magnum, 'getClusterTemplates').and.returnValue(deferred.promise);
|
spyOn(magnum, 'getClusterTemplates').and.returnValue(deferred.promise);
|
||||||
|
spyOn(magnum, 'getIngressControllers').and.returnValue(controllersDeferred.promise);
|
||||||
|
spyOn(magnum, 'getAddons').and.returnValue(addonDeferred.promise);
|
||||||
|
spyOn(nova, 'getAvailabilityZones').and.returnValue(zoneDeferred.promise);
|
||||||
spyOn(nova, 'getFlavors').and.returnValue(deferred.promise);
|
spyOn(nova, 'getFlavors').and.returnValue(deferred.promise);
|
||||||
spyOn(nova, 'getKeypairs').and.returnValue(keyDeferred.promise);
|
spyOn(nova, 'getKeypairs').and.returnValue(keyDeferred.promise);
|
||||||
|
spyOn(neutron, 'getNetworks').and.returnValue(networkDeferred.promise);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should be init', inject(function($timeout) {
|
it('should be initialised', inject(function($timeout) {
|
||||||
var config = workflow.init('create', 'Create Cluster', $scope);
|
var config;
|
||||||
|
|
||||||
|
workflow.init('Create Cluster', $scope).then(function(conf) {
|
||||||
|
config = conf;
|
||||||
|
});
|
||||||
|
|
||||||
$timeout.flush();
|
$timeout.flush();
|
||||||
|
|
||||||
expect(config.title).toBeDefined();
|
expect(config.title).toBeDefined();
|
||||||
expect(config.schema).toBeDefined();
|
expect(config.schema).toBeDefined();
|
||||||
expect(config.form).toBeDefined();
|
expect(config.form).toBeDefined();
|
||||||
expect(config.model).toBeDefined();
|
expect(config.model).toBeDefined();
|
||||||
|
expect($scope.model).toBeDefined();
|
||||||
|
expect($scope.model.DEFAULTS).toBeDefined();
|
||||||
|
|
||||||
|
expect(config.model.ingressControllers).toBe(controllersResponse.controllers);
|
||||||
|
expect(config.model.addons.length).toBe(2);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -48,6 +48,8 @@
|
|||||||
signCertificate: signCertificate,
|
signCertificate: signCertificate,
|
||||||
rotateCertificate: rotateCertificate,
|
rotateCertificate: rotateCertificate,
|
||||||
getStats: getStats,
|
getStats: getStats,
|
||||||
|
getIngressControllers: getIngressControllers,
|
||||||
|
getAddons: getAddons,
|
||||||
getQuotas: getQuotas,
|
getQuotas: getQuotas,
|
||||||
getQuota: getQuota,
|
getQuota: getQuota,
|
||||||
createQuota: createQuota,
|
createQuota: createQuota,
|
||||||
@ -213,6 +215,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// Ingress //
|
||||||
|
// Controllers //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
function getIngressControllers() {
|
||||||
|
return apiService.get('/api/container_infra/ingress_controllers/')
|
||||||
|
.error(function() {
|
||||||
|
toastService.add('error',
|
||||||
|
gettext('Unable to retrieve available ingress controllers.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
// Add-Ons //
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
function getAddons() {
|
||||||
|
return apiService.get('/api/container_infra/available_addons/')
|
||||||
|
.error(function() {
|
||||||
|
toastService.add('error',
|
||||||
|
gettext('Unable to retrieve available add-ons.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//////////////
|
//////////////
|
||||||
// Quotas //
|
// Quotas //
|
||||||
//////////////
|
//////////////
|
||||||
|
@ -279,6 +279,18 @@
|
|||||||
"method": "get",
|
"method": "get",
|
||||||
"path": "/api/container_infra/stats/",
|
"path": "/api/container_infra/stats/",
|
||||||
"error": "Unable to retrieve the stats."
|
"error": "Unable to retrieve the stats."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"func": "getIngressControllers",
|
||||||
|
"method": "get",
|
||||||
|
"path": "/api/container_infra/ingress_controllers/",
|
||||||
|
"error": "Unable to retrieve available ingress controllers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"func": "getAddons",
|
||||||
|
"method": "get",
|
||||||
|
"path": "/api/container_infra/available_addons/",
|
||||||
|
"error": "Unable to retrieve available add-ons."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- >
|
||||||
|
Improve cluster launch workflow form.
|
||||||
|
- >
|
||||||
|
Add configuration for specifying ingress controllers and addon software
|
||||||
|
supported / available for use with clusters.
|
||||||
|
- >
|
||||||
|
Adds REST endpoints for retrieving configured ingress controllers and addon
|
||||||
|
software.
|
Loading…
Reference in New Issue
Block a user