From 6ab8f75e592a060a6dddcad67e2ad995e3bd8bb1 Mon Sep 17 00:00:00 2001 From: Shu Muto Date: Wed, 14 Jun 2017 16:20:45 +0900 Subject: [PATCH] Add update action for container This patch adds update action for container as item action. Change-Id: Ie80a1f447e218213adaff2253a1ec1afd7fe5672 --- zun_ui/api/client.py | 59 ++++--- zun_ui/api/rest_api.py | 11 ++ .../container/containers/actions.module.js | 9 ++ .../containers/actions/update.service.js | 144 ++++++++++++++++++ .../actions/workflow/workflow.service.js | 9 +- .../static/dashboard/container/zun.service.js | 6 + 6 files changed, 201 insertions(+), 37 deletions(-) create mode 100644 zun_ui/static/dashboard/container/containers/actions/update.service.js diff --git a/zun_ui/api/client.py b/zun_ui/api/client.py index 2cf4921..3a92211 100644 --- a/zun_ui/api/client.py +++ b/zun_ui/api/client.py @@ -43,39 +43,37 @@ def zunclient(request): return c -def container_create(request, **kwargs): +def _cleanup_params(attrs, check, **params): args = {} run = False - for (key, value) in kwargs.items(): + + for (key, value) in params.items(): if key == "run": run = value - continue elif key == "interactive": args["interactive"] = value - continue elif key == "restart_policy": args[key] = utils.check_restart_policy(value) - continue - - if key in CONTAINER_CREATE_ATTRS: + elif key == "environment" or key == "labels": + values = {} + vals = value.split(",") + for v in vals: + kv = v.split("=", 1) + values[kv[0]] = kv[1] + args[str(key)] = values + elif key in attrs: + if value is None: + value = '' args[str(key)] = str(value) - else: + elif check: raise exceptions.BadRequest( - "Key must be in %s" % ",".join(CONTAINER_CREATE_ATTRS)) - if key == "environment": - envs = {} - vals = value.split(",") - for v in vals: - kv = v.split("=", 1) - envs[kv[0]] = kv[1] - args["environment"] = envs - elif key == "labels": - labels = {} - vals = value.split(",") - for v in vals: - kv = v.split("=", 1) - labels[kv[0]] = kv[1] - args["labels"] = labels + "Key must be in %s" % ",".join(attrs)) + + return args, run + + +def container_create(request, **kwargs): + args, run = _cleanup_params(CONTAINER_CREATE_ATTRS, True, **kwargs) response = None if run: response = zunclient(request).containers.run(**args) @@ -84,6 +82,11 @@ def container_create(request, **kwargs): return response +def container_update(request, id, **kwargs): + args, run = _cleanup_params(CONTAINER_CREATE_ATTRS, True, **kwargs) + return zunclient(request).containers.update(id, **args) + + def container_delete(request, id, force=False): # TODO(shu-mutou): force option should be provided by user. return zunclient(request).containers.delete(id, force) @@ -147,13 +150,5 @@ def image_list(request, limit=None, marker=None, sort_key=None, def image_create(request, **kwargs): - args = {} - for (key, value) in kwargs.items(): - - if key in IMAGE_PULL_ATTRS: - args[str(key)] = str(value) - else: - raise exceptions.BadRequest( - "Key must be in %s" % ",".join(IMAGE_PULL_ATTRS)) - + args = _cleanup_params(IMAGE_PULL_ATTRS, True, **kwargs) return zunclient(request).images.create(**args) diff --git a/zun_ui/api/rest_api.py b/zun_ui/api/rest_api.py index 0421c80..cc7dc28 100644 --- a/zun_ui/api/rest_api.py +++ b/zun_ui/api/rest_api.py @@ -46,6 +46,17 @@ class Container(generic.View): """ return client.container_delete(request, id, force=True) + @rest_utils.ajax(data_required=True) + def patch(self, request, id): + """Update a Container. + + Returns the Container object on success. + """ + container = client.container_update(request, id, **request.DATA) + return rest_utils.CreatedResponse( + '/api/zun/containers/%s' % id, + container.to_dict()) + @urls.register class ContainerActions(generic.View): diff --git a/zun_ui/static/dashboard/container/containers/actions.module.js b/zun_ui/static/dashboard/container/containers/actions.module.js index cb6189a..a684961 100644 --- a/zun_ui/static/dashboard/container/containers/actions.module.js +++ b/zun_ui/static/dashboard/container/containers/actions.module.js @@ -33,6 +33,7 @@ 'horizon.framework.conf.resource-type-registry.service', 'horizon.framework.util.i18n.gettext', 'horizon.dashboard.container.containers.create.service', + 'horizon.dashboard.container.containers.update.service', 'horizon.dashboard.container.containers.delete.service', 'horizon.dashboard.container.containers.delete-force.service', 'horizon.dashboard.container.containers.start.service', @@ -50,6 +51,7 @@ registry, gettext, createContainerService, + updateContainerService, deleteContainerService, deleteContainerForceService, startContainerService, @@ -92,6 +94,13 @@ text: gettext('Refresh') } }) + .append({ + id: 'updateContainerAction', + service: updateContainerService, + template: { + text: gettext('Update Container') + } + }) .append({ id: 'startContainerAction', service: startContainerService, diff --git a/zun_ui/static/dashboard/container/containers/actions/update.service.js b/zun_ui/static/dashboard/container/containers/actions/update.service.js new file mode 100644 index 0000000..8067b05 --- /dev/null +++ b/zun_ui/static/dashboard/container/containers/actions/update.service.js @@ -0,0 +1,144 @@ +/** + * 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.containers.update.service + * @description Service for the container update modal + */ + angular + .module('horizon.dashboard.container.containers') + .factory('horizon.dashboard.container.containers.update.service', updateService); + + updateService.$inject = [ + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.zun', + 'horizon.dashboard.container.containers.resourceType', + 'horizon.dashboard.container.containers.workflow', + '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' + ]; + + function updateService( + policy, zun, resourceType, workflow, + actionResult, gettext, $qExtensions, modal, toast + ) { + var message = { + success: gettext('Container %s was successfully updated.') + }; + + var service = { + initAction: initAction, + perform: perform, + allowed: allowed + }; + + return service; + + ////////////// + + function initAction() { + } + + function perform(selected) { + var title, submitText; + title = gettext('Update Container'); + submitText = gettext('Update'); + var config = workflow.init('update', title, submitText); + config.model.id = selected.id; + + // load current data + zun.getContainer(selected.id).then(onLoad); + function onLoad(response) { + config.model.name = response.data.name + ? response.data.name : ""; + config.model.image = response.data.image + ? response.data.image : ""; + config.model.image_driver = response.data.image_driver + ? response.data.image_driver : "docker"; + config.model.image_pull_policy = response.data.image_pull_policy + ? response.data.image_pull_policy : ""; + config.model.command = response.data.command + ? response.data.command : ""; + config.model.cpu = response.data.cpu + ? response.data.cpu : ""; + config.model.memory = response.data.memory + ? parseInt(response.data.memory, 10) : ""; + config.model.restart_policy = response.data.restart_policy.Name + ? response.data.restart_policy.Name : ""; + config.model.restart_policy_max_retry = response.data.restart_policy.MaximumRetryCount + ? parseInt(response.data.restart_policy.MaximumRetryCount, 10) : null; + config.model.workdir = response.data.workdir + ? response.data.workdir : ""; + config.model.environment = response.data.environment + ? hashToString(response.data.environment) : ""; + config.model.interactive = response.data.interactive + ? response.data.interactive : false; + config.model.labels = response.data.labels + ? hashToString(response.data.labels) : ""; + } + + return modal.open(config).then(submit); + } + + function allowed() { + return policy.ifAllowed({ rules: [['container', 'edit_container']] }); + } + + function submit(context) { + var id = context.model.id; + context.model = cleanUpdateProperties(context.model); + return zun.updateContainer(id, context.model).then(success); + } + + function success(response) { + response.data.id = response.data.uuid; + toast.add('success', interpolate(message.success, [response.data.name])); + var result = actionResult.getActionResult().updated(resourceType, response.data.name); + return result.result; + } + + function cleanUpdateProperties(model) { + // Initially clean fields that don't have any value. + // Not only "null", blank too. + // only "cpu" and "memory" fields are editable. + for (var key in model) { + if (model.hasOwnProperty(key) && model[key] === null || model[key] === "" || + (key !== "cpu" && key !== "memory")) { + delete model[key]; + } + } + return model; + } + + function hashToString(hash) { + var str = ""; + for (var key in hash) { + if (hash.hasOwnProperty(key)) { + if (str.length > 0) { + str += ","; + } + str += key + "=" + hash[key]; + } + } + return str; + } + } +})(); diff --git a/zun_ui/static/dashboard/container/containers/actions/workflow/workflow.service.js b/zun_ui/static/dashboard/container/containers/actions/workflow/workflow.service.js index f9272ae..8e199a2 100644 --- a/zun_ui/static/dashboard/container/containers/actions/workflow/workflow.service.js +++ b/zun_ui/static/dashboard/container/containers/actions/workflow/workflow.service.js @@ -82,8 +82,7 @@ cpu: { title: gettext("CPU"), type: "number", - minimum: 0, - step: 0.1 + minimum: 0 }, memory: { title: gettext("Memory"), @@ -182,7 +181,8 @@ }, { key: "run", - readonly: action === "update" + readonly: action === "update", + condition: action === "update" } ] } @@ -200,6 +200,7 @@ items: [ { key: "cpu", + step: 0.1, placeholder: gettext("The number of virtual cpu for this container.") }, { @@ -292,8 +293,6 @@ ]; // model model = { - //id: "", - //uuid: "", // info name: "", image: "", diff --git a/zun_ui/static/dashboard/container/zun.service.js b/zun_ui/static/dashboard/container/zun.service.js index 9754bc1..f168ad4 100644 --- a/zun_ui/static/dashboard/container/zun.service.js +++ b/zun_ui/static/dashboard/container/zun.service.js @@ -29,6 +29,7 @@ var imagesPath = '/api/zun/images/'; var service = { createContainer: createContainer, + updateContainer: updateContainer, getContainer: getContainer, getContainers: getContainers, deleteContainer: deleteContainer, @@ -57,6 +58,11 @@ return apiService.post(containersPath, params).error(error(msg)); } + function updateContainer(id, params) { + var msg = gettext('Unable to update Container.'); + return apiService.patch(containersPath + id, params).error(error(msg)); + } + function getContainer(id) { var msg = gettext('Unable to retrieve the Container.'); return apiService.get(containersPath + id).error(error(msg));