From 6d40d87987033947b41515e18109c0596bf42495 Mon Sep 17 00:00:00 2001 From: Alexander Tivelkov Date: Wed, 28 Sep 2016 18:30:23 +0300 Subject: [PATCH] Ability to assign metadata to Murano Applications and Environments Applications deployed in Murano Environments can now be assigned with metadata attributes which are passed to their object model as a "metadata" block in their ?-header. A standard metadata assignment modal dialog is utilized for this purpose. Same action and dialog is available for Murano Environments as well. Implementation adds a horizon-hosted rest API which handles requests from Angular-based scripts and forwards them to murano-api. To properly support the new api in metadata modal dialog a Horizon core metadata service is decorated during the configuration of murano module (explicitly added in enabled/_50_murano.py). This allows us to reuse the latest version of upstream metadata service and put murano-related calls on top of it. Co-Authored-By: Timur Sufiev Change-Id: I348629aedc3e9731616a53d731c33fb442ee12ec Targets-blueprint: metadata-assignment-and-propagation --- muranodashboard/api/rest/__init__.py | 1 + muranodashboard/api/rest/environments.py | 158 +++++++++++++++++ muranodashboard/environments/api.py | 21 +++ muranodashboard/environments/tables.py | 69 +++++++- muranodashboard/local/enabled/_50_murano.py | 3 + .../core/metadata/metadata.service.spec.js | 163 ++++++++++++++++++ .../static/app/murano/murano.module.js | 100 +++++++++++ .../static/app/murano/murano.module.spec.js | 113 ++++++++++++ .../static/app/murano/murano.service.js | 131 +++++++++++++- .../static/app/murano/murano.service.spec.js | 108 ++++++++++++ 10 files changed, 864 insertions(+), 3 deletions(-) create mode 100644 muranodashboard/api/rest/environments.py create mode 100644 muranodashboard/static/app/core/metadata/metadata.service.spec.js create mode 100644 muranodashboard/static/app/murano/murano.module.js create mode 100644 muranodashboard/static/app/murano/murano.module.spec.js create mode 100644 muranodashboard/static/app/murano/murano.service.spec.js diff --git a/muranodashboard/api/rest/__init__.py b/muranodashboard/api/rest/__init__.py index 4fb5db83f..e4bd5131b 100644 --- a/muranodashboard/api/rest/__init__.py +++ b/muranodashboard/api/rest/__init__.py @@ -11,4 +11,5 @@ # limitations under the License. # import REST API modules here +from . import environments # noqa from . import packages # noqa diff --git a/muranodashboard/api/rest/environments.py b/muranodashboard/api/rest/environments.py new file mode 100644 index 000000000..16386cc25 --- /dev/null +++ b/muranodashboard/api/rest/environments.py @@ -0,0 +1,158 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + + +from django.views import generic + +from openstack_dashboard.api.rest import urls +from openstack_dashboard.api.rest import utils as rest_utils + +from muranodashboard import api +from muranodashboard.environments import api as env_api + + +@urls.register +class ComponentsMetadata(generic.View): + """API for Murano components Metadata""" + + url_regex = r'app-catalog/environments/(?P[^/]+)' \ + r'/components/(?P[^/]+)/metadata/$' + + @rest_utils.ajax() + def get(self, request, environment, component): + """Get a metadata object for a component in a given environment + + Example GET: + http://localhost/api/app-catalog/environments/123/components/456/metadata + + The following get parameters may be passed in the GET + request: + + :param environment: identifier of the environment + :param component: identifier of the component + + Any additionally a "session" parameter should be passed through the API + as a keyword. + """ + filters, keywords = rest_utils.parse_filters_kwargs(request, + ['session']) + session = keywords.get('session') + if not session: + session = env_api.Session.get_or_create_or_delete(request, + environment) + component = api.muranoclient(request).services.get( + environment, '/' + component, session) + if component: + return component.to_dict()['?'].get('metadata', {}) + return {} + + @rest_utils.ajax(data_required=True) + def post(self, request, environment, component): + """Set a metadata object for a component in a given environment + + Example POST: + http://localhost/api/app-catalog/environments/123/components/456/metadata + + The following get parameters may be passed in the GET + request: + + :param environment: identifier of the environment + :param component: identifier of the component + + Any additionally a "session" parameter should be passed through the API + as a keyword. Request body should contain 'updated' keyword, contain + all the updated metadata attributes. If it is empty, the metadata is + considered to be deleted. + """ + client = api.muranoclient(request) + filters, keywords = rest_utils.parse_filters_kwargs(request, + ['session']) + session = keywords.get('session') + if not session: + session = env_api.Session.get_or_create_or_delete(request, + environment) + updated = request.DATA.get('updated', {}) + path = '/{0}/%3F/metadata'.format(component) + + if updated: + client.services.put(environment, path, updated, session) + else: + client.services.delete(environment, path, session) + + +@urls.register +class EnvironmentsMetadata(generic.View): + """API for Murano components Metadata""" + + url_regex = r'app-catalog/environments/(?P[^/]+)/metadata/$' + + @rest_utils.ajax() + def get(self, request, environment): + """Get a metadata object for an environment + + Example GET: + http://localhost/api/app-catalog/environments/123/metadata + + The following get parameters may be passed in the GET + request: + + :param environment: identifier of the environment + + Any additionally a "session" parameter should be passed through the API + as a keyword. + """ + filters, keywords = rest_utils.parse_filters_kwargs(request, + ['session']) + session = keywords.get('session') + if not session: + session = env_api.Session.get_or_create_or_delete(request, + environment) + env = api.muranoclient(request).environments.get_model( + environment, '/', session) + if env: + return env['?'].get('metadata', {}) + return {} + + @rest_utils.ajax(data_required=True) + def post(self, request, environment): + """Set a metadata object for a given environment + + Example POST: + http://localhost/api/app-catalog/environments/123/metadata + + The following get parameters may be passed in the GET + request: + + :param environment: identifier of the environment + + Any additionally a "session" parameter should be passed through the API + as a keyword. Request body should contain 'updated' keyword, contain + all the updated metadata attributes. If it is empty, the metadata is + considered to be deleted. + """ + client = api.muranoclient(request) + filters, keywords = rest_utils.parse_filters_kwargs(request, + ['session']) + + session = keywords.get('session') + if not session: + session = env_api.Session.get_or_create_or_delete(request, + environment) + updated = request.DATA.get('updated', {}) + patch = { + "op": "replace", + "path": "/?/metadata", + "value": updated + } + client.environments.update_model(environment, [patch], session) diff --git a/muranodashboard/environments/api.py b/muranodashboard/environments/api.py index 152b50ff0..b0d3c6369 100644 --- a/muranodashboard/environments/api.py +++ b/muranodashboard/environments/api.py @@ -117,6 +117,27 @@ class Session(object): "for the environment {0}".format(environment_id)) return id + @staticmethod + def get_if_available(request, environment_id): + """Get an id of open session if it exists and is not in deployed state. + + Returns None otherwise + """ + sessions = request.session.get('sessions', {}) + client = api.muranoclient(request) + + if environment_id in sessions: + id = sessions[environment_id] + try: + session_data = client.sessions.get(environment_id, id) + except exc.HTTPForbidden: + return None + else: + if session_data.state in [consts.STATUS_ID_DEPLOY_FAILURE, + consts.STATUS_ID_READY]: + return None + return id + @staticmethod def get(request, environment_id): """Get an open session id diff --git a/muranodashboard/environments/tables.py b/muranodashboard/environments/tables.py index 7ce746f13..a30b0e2a6 100644 --- a/muranodashboard/environments/tables.py +++ b/muranodashboard/environments/tables.py @@ -433,6 +433,36 @@ class UpdateName(tables.UpdateAction): return True +class UpdateEnvMetadata(tables.LinkAction): + name = "update_env_metadata" + verbose_name = _("Update Metadata") + ajax = False + icon = "pencil" + attrs = {"ng-controller": "MetadataModalHelperController as modal"} + + def __init__(self, attrs=None, **kwargs): + kwargs['preempt'] = True + self.session_id = None + super(UpdateEnvMetadata, self).__init__(attrs, **kwargs) + + def get_link_url(self, environment): + target = { + 'environment': str(environment.id), + 'session': str(self.session_id), + } + self.attrs['ng-click'] = ( + "modal.openMetadataModal('muranoenv', %s, true)" % target) + return "javascript:void(0);" + + def allowed(self, request, environment=None): + return environment.status not in (consts.STATUS_ID_DEPLOYING, + consts.STATUS_ID_DELETING) + + def update(self, request, datum): + env_id = self.table.kwargs.get('environment_id') + self.session_id = api.Session.get_if_available(request, env_id) + + class EnvironmentsTable(tables.DataTable): name = md_utils.Column( 'name', @@ -472,7 +502,8 @@ class EnvironmentsTable(tables.DataTable): table_actions = (CreateEnvironment, DeployEnvironment, DeleteEnvironment, AbandonEnvironment) row_actions = (ShowEnvironmentServices, DeployEnvironment, - DeleteEnvironment, AbandonEnvironment) + DeleteEnvironment, AbandonEnvironment, + UpdateEnvMetadata) def get_service_details_link(service): @@ -484,6 +515,40 @@ def get_service_type(datum): return datum['?'].get(consts.DASHBOARD_ATTRS_KEY, {}).get('name') +class UpdateMetadata(tables.LinkAction): + name = "update_metadata" + verbose_name = _("Update Metadata") + ajax = False + icon = "pencil" + attrs = {"ng-controller": "MetadataModalHelperController as modal"} + + def __init__(self, attrs=None, **kwargs): + kwargs['preempt'] = True + self.session_id = None + super(UpdateMetadata, self).__init__(attrs, **kwargs) + + def get_link_url(self, service): + env_id = self.table.kwargs.get('environment_id') + comp_id = service['?']['id'] + target = { + 'environment': str(env_id), + 'component': str(comp_id), + 'session': str(self.session_id), + } + self.attrs['ng-click'] = ( + "modal.openMetadataModal('muranoapp', %s, true)" % target) + return "javascript:void(0);" + + def allowed(self, request, service=None): + status, version = _get_environment_status_and_version(request, + self.table) + return status != consts.STATUS_ID_DEPLOYING + + def update(self, request, datum): + env_id = self.table.kwargs.get('environment_id') + self.session_id = api.Session.get_if_available(request, env_id) + + class ServicesTable(tables.DataTable): name = md_utils.Column( 'name', @@ -572,7 +637,7 @@ class ServicesTable(tables.DataTable): status_columns = ['status'] row_class = UpdateServiceRow table_actions = (AddApplication, DeployThisEnvironment) - row_actions = (DeleteService,) + row_actions = (DeleteService, UpdateMetadata) multi_select = False diff --git a/muranodashboard/local/enabled/_50_murano.py b/muranodashboard/local/enabled/_50_murano.py index 8fd029356..fdbebff47 100644 --- a/muranodashboard/local/enabled/_50_murano.py +++ b/muranodashboard/local/enabled/_50_murano.py @@ -28,11 +28,14 @@ ADD_EXCEPTIONS = { 'unauthorized': exceptions.UNAUTHORIZED, } +ADD_ANGULAR_MODULES = ['horizon.app.murano'] + ADD_JS_FILES = [ 'muranodashboard/js/upload_form.js', 'muranodashboard/js/import_bundle_form.js', 'muranodashboard/js/more-less.js', 'app/murano/murano.service.js', + 'app/murano/murano.module.js' ] diff --git a/muranodashboard/static/app/core/metadata/metadata.service.spec.js b/muranodashboard/static/app/core/metadata/metadata.service.spec.js new file mode 100644 index 000000000..3ce756c01 --- /dev/null +++ b/muranodashboard/static/app/core/metadata/metadata.service.spec.js @@ -0,0 +1,163 @@ +/* + * Copyright 2015, ThoughtWorks Inc. + * + * 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('metadata.service', function () { + + /** + * That's almost a copy-paste of original metadata.service.spec.js file, + * with the exception that a murano module is added to modules registry to + * check that murano decorator for metadata service doesn't break core + * OpenStack services metadata. + */ + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.murano')); + + var nova = {getAggregateExtraSpecs: function() {}, + getFlavorExtraSpecs: function() {}, + editAggregateExtraSpecs: function() {}, + editFlavorExtraSpecs: function() {}, + getInstanceMetadata: function() {}, + editInstanceMetadata: function() {} }; + + var glance = {getImageProps: function() {}, + editImageProps: function() {}, + getNamespaces: function() {}}; + + var cinder = {getVolumeMetadata:function() {}, + getVolumeSnapshotMetadata:function() {}, + getVolumeTypeMetadata:function() {}, + editVolumeMetadata: function() {}, + editVolumeSnapshotMetadata: function() {}}; + + beforeEach(function() { + module(function($provide) { + $provide.value('horizon.app.core.openstack-service-api.nova', nova); + $provide.value('horizon.app.core.openstack-service-api.glance', glance); + $provide.value('horizon.app.core.openstack-service-api.cinder', cinder); + }); + }); + + var metadataService; + + beforeEach(inject(function($injector) { + metadataService = $injector.get('horizon.app.core.metadata.service'); + })); + + it('should get aggregate metadata', function() { + var expected = 'aggregate metadata'; + spyOn(nova, 'getAggregateExtraSpecs').and.returnValue(expected); + var actual = metadataService.getMetadata('aggregate', '1'); + expect(actual).toBe(expected); + }); + + it('should edit aggregate metadata', function() { + spyOn(nova, 'editAggregateExtraSpecs'); + metadataService.editMetadata('aggregate', '1', 'updated', ['removed']); + expect(nova.editAggregateExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should get aggregate namespace', function() { + spyOn(glance, 'getNamespaces'); + metadataService.getNamespaces('aggregate'); + expect(glance.getNamespaces) + .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Aggregate' }, false); + }); + + it('should get flavor metadata', function() { + var expected = 'flavor metadata'; + spyOn(nova, 'getFlavorExtraSpecs').and.returnValue(expected); + var actual = metadataService.getMetadata('flavor', '1'); + expect(actual).toBe(expected); + }); + + it('should edit flavor metadata', function() { + spyOn(nova, 'editFlavorExtraSpecs'); + metadataService.editMetadata('flavor', '1', 'updated', ['removed']); + expect(nova.editFlavorExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should get flavor namespace', function() { + spyOn(glance, 'getNamespaces'); + metadataService.getNamespaces('flavor'); + expect(glance.getNamespaces) + .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Flavor' }, false); + }); + + it('should get image metadata', function() { + var expected = 'image metadata'; + spyOn(glance, 'getImageProps').and.returnValue(expected); + var actual = metadataService.getMetadata('image', '1'); + expect(actual).toBe(expected); + }); + + it('should edit image metadata', function() { + spyOn(glance, 'editImageProps'); + metadataService.editMetadata('image', '1', 'updated', ['removed']); + expect(glance.editImageProps).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should edit volume metadata', function() { + spyOn(cinder, 'editVolumeMetadata'); + metadataService.editMetadata('volume', '1', 'updated', ['removed']); + expect(cinder.editVolumeMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should edit volume snapshot metadata', function() { + spyOn(cinder, 'editVolumeSnapshotMetadata'); + metadataService.editMetadata('volume_snapshot', '1', 'updated', ['removed']); + expect(cinder.editVolumeSnapshotMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should get image namespace', function() { + spyOn(glance, 'getNamespaces'); + metadataService.getNamespaces('image'); + expect(glance.getNamespaces) + .toHaveBeenCalledWith({ resource_type: 'OS::Glance::Image' }, false); + }); + + it('should get instance metadata', function() { + var expected = 'instance metadata'; + spyOn(nova, 'getInstanceMetadata').and.returnValue(expected); + var actual = metadataService.getMetadata('instance', '1'); + expect(actual).toBe(expected); + }); + + it('should get volume metadata', function() { + var expected = 'volume metadata'; + spyOn(cinder, 'getVolumeMetadata').and.returnValue(expected); + var actual = metadataService.getMetadata('volume', '1'); + expect(actual).toBe(expected); + }); + + it('should edit instance metadata', function() { + spyOn(nova, 'editInstanceMetadata'); + metadataService.editMetadata('instance', '1', 'updated', ['removed']); + expect(nova.editInstanceMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should get instance namespace', function() { + spyOn(glance, 'getNamespaces'); + metadataService.getNamespaces('instance', 'metadata'); + expect(glance.getNamespaces) + .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Server', + properties_target: 'metadata' }, false); + }); + + }); + +})(); diff --git a/muranodashboard/static/app/murano/murano.module.js b/muranodashboard/static/app/murano/murano.module.js new file mode 100644 index 000000000..4a3f40fb9 --- /dev/null +++ b/muranodashboard/static/app/murano/murano.module.js @@ -0,0 +1,100 @@ +/** + * Copyright 2016, Mirantis, Inc. + * + * 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 horizon.app.murano + * @ng-module + * @description + * Provides all of the services and widgets required + * to support and display the Murano Packages panel. + */ + angular + .module('horizon.app.murano', []) + .config(config); + + config.$inject = [ + '$injector', + '$provide' + ]; + + function config($injector, $provide) { + if ($injector.has('horizon.app.core.metadata.service')) { + $provide.decorator('horizon.app.core.metadata.service', patchMetadata); + } + + patchMetadata.$inject = [ + '$delegate', + 'horizon.app.core.openstack-service-api.murano', + 'horizon.app.core.openstack-service-api.glance' + ]; + function patchMetadata($delegate, murano, glance) { + var origEditMetadata = $delegate.editMetadata; + var origGetMetadata = $delegate.getMetadata; + var origGetNamespaces = $delegate.getNamespaces; + + $delegate.editMetadata = editMetadata; + $delegate.getMetadata = getMetadata; + $delegate.getNamespaces = getNamespaces; + + return $delegate; + + function getMetadata(resource, id) { + if (resource === 'muranoapp') { + return murano.getComponentMeta(id); + } + if (resource === 'muranoenv') { + return murano.getEnvironmentMeta(id); + } + return origGetMetadata(resource, id); + } + + function editMetadata(resource, id, updated, removed) { + if (resource === 'muranoapp') { + return murano.editComponentMeta(id, updated, removed); + } + if (resource === 'muranoenv') { + return murano.editEnvironmentMeta(id, updated, removed); + } + return origEditMetadata(resource, id, updated, removed); + } + + function getNamespaces(resource, propertiesTarget) { + var params; + if (resource === 'muranoapp') { + params = {resource_type: 'OS::Murano::Application'}; + if (propertiesTarget) { + params.properties_target = propertiesTarget; + } + return glance.getNamespaces(params, false); + } + if (resource === 'muranoenv') { + params = {resource_type: 'OS::Murano::Environment'}; + if (propertiesTarget) { + params.properties_target = propertiesTarget; + } + return glance.getNamespaces(params, false); + } + + return origGetNamespaces(resource, propertiesTarget); + + } + } + } + +})(); diff --git a/muranodashboard/static/app/murano/murano.module.spec.js b/muranodashboard/static/app/murano/murano.module.spec.js new file mode 100644 index 000000000..c25233300 --- /dev/null +++ b/muranodashboard/static/app/murano/murano.module.spec.js @@ -0,0 +1,113 @@ +/** + * Copyright 2016, Mirantis, Inc. + * + * 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.app.murano', function () { + it('should be defined', function () { + expect(angular.module('horizon.app.murano')).toBeDefined(); + }); + }); + + describe('murano metadata patcher', function () { + var metadataService, glance, murano; + + var lastFakeCallArgs = []; + var fakeResult = 'fakeMeta'; + + beforeEach(function () { + murano = { + getComponentMeta: fakeMeta, + editComponentMeta: fakeMeta, + getEnvironmentMeta: fakeMeta, + editEnvironmentMeta: fakeMeta + }; + module('horizon.framework'); + module('horizon.app.core'); + module('horizon.app.murano'); + module(function($provide) { + $provide.value('horizon.app.core.openstack-service-api.murano', murano); + }); + + function fakeMeta() { + var i; + for (i = 0, lastFakeCallArgs = []; i < arguments.length; i++) { + lastFakeCallArgs.push(arguments[i]); + } + return fakeResult; + } + }); + + beforeEach(inject(function ($injector) { + metadataService = $injector.get('horizon.app.core.metadata.service'); + glance = $injector.get('horizon.app.core.openstack-service-api.glance'); + })); + + it('should get component metadata', function () { + var actual = metadataService.getMetadata('muranoapp', 'compId'); + expect(actual).toBe(fakeResult); + expect(lastFakeCallArgs).toEqual(['compId']); + }); + + it('should get environment metadata', function () { + var actual = metadataService.getMetadata('muranoenv', 'envId'); + expect(actual).toBe(fakeResult); + expect(lastFakeCallArgs).toEqual(['envId']); + }); + + it('should edit component metadata', function () { + metadataService.editMetadata('muranoapp', 'compId', + {'key1': 10}, ['key2']); + expect(lastFakeCallArgs).toEqual(['compId', {'key1': 10}, ['key2']]); + }); + + it('should edit environment metadata', function () { + metadataService.editMetadata('muranoenv', 'envId', + {'key1': 10}, ['key2']); + expect(lastFakeCallArgs).toEqual(['envId', {'key1': 10}, ['key2']]); + }); + + it('should get component namespace', function () { + var params, flag; + spyOn(glance, 'getNamespaces').and.callFake(function(_params, _flag) { + params = _params; + flag = _flag; + }); + metadataService.getNamespaces('muranoapp', 'something'); + expect(params).toEqual({ + resource_type: 'OS::Murano::Application', + properties_target: 'something' + }); + expect(flag).toBe(false); + }); + + it('should get environment namespace', function () { + var params, flag; + spyOn(glance, 'getNamespaces').and.callFake(function(_params, _flag) { + params = _params; + flag = _flag; + }); + metadataService.getNamespaces('muranoenv', 'something'); + expect(params).toEqual({ + resource_type: 'OS::Murano::Environment', + properties_target: 'something' + }); + expect(flag).toBe(false); + }); + + }); + +})(); diff --git a/muranodashboard/static/app/murano/murano.service.js b/muranodashboard/static/app/murano/murano.service.js index 9f65b6372..6ac030984 100644 --- a/muranodashboard/static/app/murano/murano.service.js +++ b/muranodashboard/static/app/murano/murano.service.js @@ -31,7 +31,11 @@ function muranoAPI(apiService, toastService) { var service = { - getPackages: getPackages + getPackages: getPackages, + getComponentMeta: getComponentMeta, + editComponentMeta: editComponentMeta, + getEnvironmentMeta: getEnvironmentMeta, + editEnvironmentMeta: editEnvironmentMeta }; return service; @@ -70,5 +74,130 @@ toastService.add('error', gettext('Unable to retrieve the packages.')); }); } + + /** + * @name horizon.app.core.openstack-service-api.murano.getComponentMeta + * @description + * Get metadata attributes associated with a given component + * + * @param {Object} target + * The object identifying the target component + * + * @param {string} target.environment + * The identifier of the environment the component belongs to + * + * @param {string} target.component + * The identifier of the component within the environment + * + * @param {string} target.session + * The identifier of the configuration session for which the data should be + * fetched + * + * @returns {Object} The metadata object + */ + function getComponentMeta(target) { + var params = { params: { session: target.session} }; + var url = '/api/app-catalog/environments/' + target.environment + + '/components/' + target.component + '/metadata/'; + return apiService.get(url, params) + .error(function () { + toastService.add('error', gettext('Unable to retrieve component metadata.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.murano.editComponentMetadata + * @description + * Update metadata attributes associated with a given component + * + * @param {Object} target + * The object identifying the target component + * + * @param {string} target.environment + * The identifier of the environment the component belongs to + * + * @param {string} target.component + * The identifier of the component within the environment + * + * @param {string} target.session + * The identifier of the configuration session for which the data should be + * updated + * + * @param {object} updated New metadata definitions. + * + * @param {[]} removed Names of removed metadata definitions. + * + * @returns {Object} The result of the API call + */ + function editComponentMeta(target, updated, removed) { + var params = { params: { session: target.session} }; + var url = '/api/app-catalog/environments/' + target.environment + + '/components/' + target.component + '/metadata/'; + return apiService.post( + url, { updated: updated, removed: removed}, params) + .error(function () { + toastService.add('error', gettext('Unable to edit component metadata.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.murano.getEnvironmentMeta + * @description + * Get metadata attributes associated with a given environment + * + * @param {Object} target + * The object identifying the target environment + * + * @param {string} target.environment + * The identifier of the target environment + * + * @param {string} target.session + * The identifier of the configuration session for which the data should be + * fetched + * + * @returns {Object} The metadata object + */ + function getEnvironmentMeta(target) { + var params = { params: { session: target.session} }; + var url = '/api/app-catalog/environments/' + target.environment + + '/metadata/'; + return apiService.get(url, params) + .error(function () { + toastService.add('error', gettext('Unable to retrieve environment metadata.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.murano.editEnvironmentMeta + * @description + * Update metadata attributes associated with a given environment + * + * @param {Object} target + * The object identifying the target environment + * + * @param {string} target.environment + * The identifier of the environment the component belongs to + * + * @param {string} target.session + * The identifier of the configuration session for which the data should be + * updated + * + * @param {object} updated New metadata definitions. + * + * @param {[]} removed Names of removed metadata definitions. + * + * @returns {Object} The result of the API call + */ + function editEnvironmentMeta(target, updated, removed) { + var params = { params: { session: target.session} }; + var url = '/api/app-catalog/environments/' + target.environment + + '/metadata/'; + return apiService.post( + url, { updated: updated, removed: removed}, params) + .error(function () { + toastService.add('error', gettext('Unable to edit environment metadata.')); + }); + } + } })(); diff --git a/muranodashboard/static/app/murano/murano.service.spec.js b/muranodashboard/static/app/murano/murano.service.spec.js new file mode 100644 index 000000000..08d4c38c4 --- /dev/null +++ b/muranodashboard/static/app/murano/murano.service.spec.js @@ -0,0 +1,108 @@ +/** + * Copyright 2016, Mirantis, Inc. + * + * 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('Murano API', function () { + var testCall, service; + var apiService = {}; + var toastService = {}; + + beforeEach( + module('horizon.mock.openstack-service-api', + function($provide, initServices) { + testCall = initServices($provide, apiService, toastService); + }) + ); + + beforeEach(module('horizon.app.core.openstack-service-api')); + + beforeEach(inject([ + 'horizon.app.core.openstack-service-api.murano', + function(muranoAPI) { + service = muranoAPI; + }])); + + it('defines the service', function () { + expect(service).toBeDefined(); + }); + + var tests = [ + { + func: 'getPackages', + method: 'get', + path: '/api/app-catalog/packages/', + data: {params: 'config'}, + error: 'Unable to retrieve the packages.', + testInput: [ 'config' ] + }, { + func: 'getComponentMeta', + method: 'get', + path: '/api/app-catalog/environments/1/components/2/metadata/', + data: {params: {session: 'sessionId'}}, + error: 'Unable to retrieve component metadata.', + testInput: [{session: 'sessionId', environment: '1', component: '2'}] + }, { + func: 'editComponentMeta', + method: 'post', + path: '/api/app-catalog/environments/1/components/2/metadata/', + call_args: [ + '/api/app-catalog/environments/1/components/2/metadata/', + {updated: {'key1': 10}, removed: ['key2']}, + {params: {session: 'sessionId'}} + ], + error: 'Unable to edit component metadata.', + testInput: [ + {session: 'sessionId', environment: '1', component: '2'}, + {'key1': 10}, + ['key2'] + ] + }, { + func: 'getEnvironmentMeta', + method: 'get', + path: '/api/app-catalog/environments/1/metadata/', + data: {params: {session: 'sessionId'}}, + error: 'Unable to retrieve environment metadata.', + testInput: [{session: 'sessionId', environment: '1'}] + }, { + func: 'editEnvironmentMeta', + method: 'post', + path: '/api/app-catalog/environments/1/metadata/', + call_args: [ + '/api/app-catalog/environments/1/metadata/', + {updated: {'key1': 10}, removed: ['key2']}, + {params: {session: 'sessionId'}} + ], + error: 'Unable to edit environment metadata.', + testInput: [ + {session: 'sessionId', environment: '1'}, + {'key1': 10}, + ['key2'] + ] + } + ]; + + // Iterate through the defined tests and apply as Jasmine specs. + angular.forEach(tests, function(params) { + it('defines the ' + params.func + ' call properly', function () { + var callParams = [apiService, service, toastService, params]; + testCall.apply(this, callParams); + }); + }); + + }); + +})();