diff --git a/tuning_box/app.py b/tuning_box/app.py index f185ff8..018d306 100644 --- a/tuning_box/app.py +++ b/tuning_box/app.py @@ -59,12 +59,25 @@ api.add_resource( '/environments//resources/' '/values' ) +api.add_resource( + resource_values.ResourceValuesKeys, + '/environments//resources/' + '/values/' + 'keys/' +) # Resource overrides api.add_resource( resource_overrides.ResourceOverrides, '/environments//' - 'resources//overrides') + 'resources//overrides' +) +api.add_resource( + resource_overrides.ResourceOverridesKeys, + '/environments//' + 'resources//overrides/' + 'keys/' +) # Environments api.add_resource(environments.EnvironmentsCollection, '/environments') diff --git a/tuning_box/library/__init__.py b/tuning_box/library/__init__.py index 9208bae..f8383fe 100644 --- a/tuning_box/library/__init__.py +++ b/tuning_box/library/__init__.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import exc as sa_exc from tuning_box import db from tuning_box import errors +from tuning_box.library import levels_hierarchy def load_objects(model, ids): @@ -71,3 +72,31 @@ def get_resource_definition(id_or_name, environment_id): raise sa_exc.MultipleResultsFound return result[0] + + +def get_resource_values(environment, levels, res_def): + level_value = levels_hierarchy.get_environment_level_value( + environment, levels) + res_values = db.ResourceValues.query.filter_by( + environment_id=environment.id, + resource_definition_id=res_def.id, + level_value_id=level_value.id, + ).all() + + if not res_values: + raise errors.TuningboxNotFound( + "Resource values not found by environment {0}, " + "resource definition {1}, level {2} with value {3}".format( + environment.id, res_def.id, level_value.level.name, + level_value.value + ) + ) + elif len(res_values) > 1: + raise errors.TuningboxIntegrityError( + "Found more than one resource values for environment {0}, " + "resource definition {1}, level {2} with value {3}".format( + environment.id, res_def.id, level_value.level.name, + level_value.value + ) + ) + return res_values[0] diff --git a/tuning_box/library/resource_keys_operation.py b/tuning_box/library/resource_keys_operation.py index 3001088..9903c28 100644 --- a/tuning_box/library/resource_keys_operation.py +++ b/tuning_box/library/resource_keys_operation.py @@ -12,7 +12,11 @@ import copy +import flask + +from tuning_box import db from tuning_box import errors +from tuning_box import library class KeysOperationMixin(object): @@ -116,3 +120,28 @@ class KeysOperationMixin(object): "Unknown operation: {0}. " "Allowed operations: {1}".format(operation, self.OPERATIONS) ) + + +class ResourceKeysMixin(KeysOperationMixin): + + @db.with_transaction + def _do_update(self, environment_id, levels, + resource_id_or_name, operation, storage_name): + + environment = db.Environment.query.get_or_404(environment_id) + res_def = library.get_resource_definition( + resource_id_or_name, environment_id) + + if res_def.id != resource_id_or_name: + from tuning_box.app import api + return flask.redirect(api.url_for( + self.__class__, + environment_id=environment_id, + levels=levels, + resource_id_or_name=res_def.id, + ), code=308) + + res_values = library.get_resource_values(environment, levels, res_def) + result = self.perform_operation( + operation, getattr(res_values, storage_name), flask.request.json) + setattr(res_values, storage_name, result) diff --git a/tuning_box/library/resource_overrides.py b/tuning_box/library/resource_overrides.py index afa303f..39d25bf 100644 --- a/tuning_box/library/resource_overrides.py +++ b/tuning_box/library/resource_overrides.py @@ -16,6 +16,7 @@ import flask_restful from tuning_box import db from tuning_box import library from tuning_box.library import levels_hierarchy +from tuning_box.library import resource_keys_operation class ResourceOverrides(flask_restful.Resource): @@ -23,15 +24,15 @@ class ResourceOverrides(flask_restful.Resource): @db.with_transaction def put(self, environment_id, levels, resource_id_or_name): environment = db.Environment.query.get_or_404(environment_id) - resdef = library.get_resource_definition( + res_def = library.get_resource_definition( resource_id_or_name, environment_id) - if resdef.id != resource_id_or_name: + if res_def.id != resource_id_or_name: from tuning_box.app import api return flask.redirect(api.url_for( ResourceOverrides, environment_id=environment_id, levels=levels, - resource_id_or_name=resdef.id, + resource_id_or_name=res_def.id, ), code=308) level_value = levels_hierarchy.get_environment_level_value( @@ -39,7 +40,7 @@ class ResourceOverrides(flask_restful.Resource): esv = db.get_or_create( db.ResourceValues, environment=environment, - resource_definition=resdef, + resource_definition=res_def, level_value=level_value, ) esv.overrides = flask.request.json @@ -48,25 +49,38 @@ class ResourceOverrides(flask_restful.Resource): @db.with_transaction def get(self, environment_id, resource_id_or_name, levels): environment = db.Environment.query.get_or_404(environment_id) - resdef = library.get_resource_definition( + res_def = library.get_resource_definition( resource_id_or_name, environment_id) - if resdef.id != resource_id_or_name: + if res_def.id != resource_id_or_name: from tuning_box.app import api url = api.url_for( ResourceOverrides, environment_id=environment_id, levels=levels, - resource_id_or_name=resdef.id, + resource_id_or_name=res_def.id, ) return flask.redirect(url, code=308) level_value = levels_hierarchy.get_environment_level_value( environment, levels) res_values = db.ResourceValues.query.filter_by( - resource_definition=resdef, + resource_definition=res_def, environment=environment, level_value=level_value, ).one_or_none() if not res_values: return {} return res_values.overrides + + +class ResourceOverridesKeys(flask_restful.Resource, + resource_keys_operation.ResourceKeysMixin): + + def put(self, environment_id, levels, resource_id_or_name, operation): + return self.patch(environment_id, levels, + resource_id_or_name, operation) + + def patch(self, environment_id, levels, resource_id_or_name, operation): + self._do_update(environment_id, levels, resource_id_or_name, + operation, 'overrides') + return None, 204 diff --git a/tuning_box/library/resource_values.py b/tuning_box/library/resource_values.py index 3c97b6e..341532f 100644 --- a/tuning_box/library/resource_values.py +++ b/tuning_box/library/resource_values.py @@ -17,6 +17,7 @@ import itertools from tuning_box import db from tuning_box import library from tuning_box.library import levels_hierarchy +from tuning_box.library import resource_keys_operation class ResourceValues(flask_restful.Resource): @@ -94,3 +95,16 @@ class ResourceValues(flask_restful.Resource): if not resource_values: return {} return resource_values.values + + +class ResourceValuesKeys(flask_restful.Resource, + resource_keys_operation.ResourceKeysMixin): + + def put(self, environment_id, levels, resource_id_or_name, operation): + return self.patch(environment_id, levels, + resource_id_or_name, operation) + + def patch(self, environment_id, levels, resource_id_or_name, operation): + self._do_update(environment_id, levels, resource_id_or_name, + operation, 'values') + return None, 204 diff --git a/tuning_box/tests/library/test_library.py b/tuning_box/tests/library/test_library.py index 845b811..1a39af2 100644 --- a/tuning_box/tests/library/test_library.py +++ b/tuning_box/tests/library/test_library.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from tuning_box.app import db from tuning_box import errors from tuning_box import library from tuning_box.tests.test_app import BaseTest @@ -76,3 +77,31 @@ class TestLibrary(BaseTest): self.assertEqual(res_id, actual_res.id) self.assertEqual(res_name, actual_res.name) self.assertEqual(component_id, actual_res.component_id) + + def test_get_resource_values(self): + self._fixture() + res_def_id = 5 + environment_id = 9 + values = {'k': 'v'} + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + self._add_resource_values(environment_id, res_def_id, levels, values) + + with self.app.app_context(), db.db.session.begin(): + + environment = db.Environment.query.get(environment_id) + res_def = db.ResourceDefinition.query.get(res_def_id) + res_values = library.get_resource_values( + environment, levels, res_def) + self.assertEqual(values, res_values.values) + + def test_get_resource_values_not_found(self): + self._fixture() + res_def_id = 5 + environment_id = 9 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + with self.app.app_context(), db.db.session.begin(): + environment = db.Environment.query.get(environment_id) + res_def = db.ResourceDefinition.query.get(res_def_id) + self.assertRaises(errors.TuningboxNotFound, + library.get_resource_values, environment, + levels, res_def) diff --git a/tuning_box/tests/library/test_resource_overrides.py b/tuning_box/tests/library/test_resource_overrides.py index 872d31f..40971a7 100644 --- a/tuning_box/tests/library/test_resource_overrides.py +++ b/tuning_box/tests/library/test_resource_overrides.py @@ -10,12 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools + from tuning_box import db from tuning_box.tests.test_app import BaseTest class TestResourceOverrides(BaseTest): + object_url = '/environments/{0}/{1}/resources/{2}/overrides' + object_keys_url = object_url + '/keys/{3}' + def test_put_resource_values_overrides_root(self): self._fixture() res = self.client.put('/environments/9/resources/5/overrides', @@ -43,17 +48,19 @@ class TestResourceOverrides(BaseTest): self.assertIsNotNone(resource_values) self.assertEqual(resource_values.overrides, {'k': 'v'}) level_value = resource_values.level_value + self.assertIsNotNone(level_value) self.assertEqual(level_value.level.name, 'lvl2') self.assertEqual(level_value.value, 'val2') level_value = level_value.parent + self.assertIsNotNone(level_value) self.assertEqual(level_value.level.name, 'lvl1') self.assertEqual(level_value.value, 'val1') self.assertIsNone(level_value.parent) def test_get_resource_values_local_override(self): self._fixture() - res = self.client.put('/environments/9/lvl1/1/resources/5/values', - data={'key': 'value1'}) + self.client.put('/environments/9/lvl1/1/resources/5/values', + data={'key': 'value1'}) res = self.client.put('/environments/9/lvl1/1/resources/5/overrides', data={'key': 'value2'}) self.assertEqual(res.status_code, 204) @@ -122,3 +129,100 @@ class TestResourceOverrides(BaseTest): 'http://localhost' '/environments/9/lvl1/val1/lvl2/val2/resources/5/overrides', ) + + def test_put_resource_overrides_set_operation_error(self): + self.app.config["PROPAGATE_EXCEPTIONS"] = True + self._fixture() + + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + overrides = {'key': 'val_overridden'} + self._add_resource_overrides(environment_id, res_def_id, levels, + overrides) + + data = [['a', 'b', 'c', 'value']] + obj_keys_url = self.object_keys_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id, + 'set' + ) + + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) + + def test_put_resource_overrides_set(self): + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + overrides = {'key': 'val_overridden'} + self._add_resource_overrides(environment_id, res_def_id, levels, + overrides) + + obj_url = self.object_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ) + obj_keys_url = obj_url + '/keys/set' + + data = [['key', 'key_over'], ['key_x', 'key_x_over']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(204, res.status_code) + + res = self.client.get(obj_url) + self.assertEqual(200, res.status_code) + actual = res.json + self.assertEqual({'key': 'key_over', 'key_x': 'key_x_over'}, + actual) + + def test_put_resource_overrides_delete(self): + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + overrides = {'key_0': 'val_0', 'key_1': 'val_1'} + self._add_resource_overrides(environment_id, res_def_id, levels, + overrides) + + obj_url = self.object_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ) + obj_keys_url = obj_url + '/keys/delete' + + data = [['key_0']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(204, res.status_code) + + res = self.client.get(obj_url) + self.assertEqual(200, res.status_code) + actual = res.json + self.assertEqual({'key_1': 'val_1'}, actual) + + def test_put_resource_overrides_delete_operation_error(self): + self.app.config["PROPAGATE_EXCEPTIONS"] = True + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + overrides = {'key_0': 'val_0', 'key_1': 'val_1'} + self._add_resource_overrides(environment_id, res_def_id, levels, + overrides) + + obj_keys_url = self.object_keys_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id, + 'delete' + ) + data = [['fake_key']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) + + data = [['key_0', 'val_0']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) diff --git a/tuning_box/tests/library/test_resource_values.py b/tuning_box/tests/library/test_resource_values.py index f1057aa..7ec6b86 100644 --- a/tuning_box/tests/library/test_resource_values.py +++ b/tuning_box/tests/library/test_resource_values.py @@ -10,12 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools + from tuning_box import db from tuning_box.tests.test_app import BaseTest class TestResourceValues(BaseTest): + object_url = '/environments/{0}/{1}/resources/{2}/values' + object_keys_url = object_url + '/keys/{3}' + def test_put_resource_values_root(self): self._fixture() res = self.client.put('/environments/9/resources/5/values', @@ -129,3 +134,106 @@ class TestResourceValues(BaseTest): 'http://localhost' '/environments/9/lvl1/val1/lvl2/val2/resources/5/values', ) + + def test_put_resource_values_not_found(self): + self.app.config["PROPAGATE_EXCEPTIONS"] = True + self._fixture() + + res = self.client.put( + '/environments/9/lvl1/val1/resources/5/values/keys/set', + data={} + ) + self.assertEqual(404, res.status_code) + + def test_put_resource_values_set_operation_error(self): + self.app.config["PROPAGATE_EXCEPTIONS"] = True + self._fixture() + + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + values = {'key': 'val'} + self._add_resource_values(environment_id, res_def_id, levels, values) + + data = [['a', 'b', 'c', 'value']] + obj_keys_url = self.object_keys_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id, + 'set' + ) + + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) + + def test_put_resource_values_set(self): + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + values = {'key': 'val'} + self._add_resource_values(environment_id, res_def_id, levels, values) + + obj_url = self.object_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ) + obj_keys_url = obj_url + '/keys/set' + + data = [['key', 'key_value'], ['key_x', 'key_x_value']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(204, res.status_code) + + res = self.client.get(obj_url) + self.assertEqual(200, res.status_code) + actual = res.json + self.assertEqual({'key': 'key_value', 'key_x': 'key_x_value'}, + actual) + + def test_put_resource_values_delete(self): + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + values = {'key_0': 'val_0', 'key_1': 'val_1'} + self._add_resource_values(environment_id, res_def_id, levels, values) + + obj_url = self.object_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ) + obj_keys_url = obj_url + '/keys/delete' + + data = [['key_0']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(204, res.status_code) + + res = self.client.get(obj_url) + self.assertEqual(200, res.status_code) + actual = res.json + self.assertEqual({'key_1': 'val_1'}, actual) + + def test_put_resource_values_delete_operation_error(self): + self.app.config["PROPAGATE_EXCEPTIONS"] = True + self._fixture() + environment_id = 9 + res_def_id = 5 + levels = (('lvl1', 'val1'), ('lvl2', 'val2')) + values = {'key_0': 'val_0', 'key_1': 'val_1'} + self._add_resource_values(environment_id, res_def_id, levels, values) + + obj_keys_url = self.object_keys_url.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id, + 'delete' + ) + data = [['fake_key']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) + + data = [['key_0', 'val_0']] + res = self.client.put(obj_keys_url, data=data) + self.assertEqual(409, res.status_code) diff --git a/tuning_box/tests/test_app.py b/tuning_box/tests/test_app.py index 12ff794..23df9ca 100644 --- a/tuning_box/tests/test_app.py +++ b/tuning_box/tests/test_app.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools import json from flask import testing @@ -72,6 +73,30 @@ class BaseTest(base.TestCase): environment.hierarchy_levels = hierarchy_levels db.db.session.add(environment) + def _add_resource_values(self, environment_id, res_def_id, + levels, values): + res = self.client.put( + '/environments/{0}/{1}/resources/{2}/values'.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ), + data=values + ) + self.assertEqual(res.status_code, 204) + + def _add_resource_overrides(self, environment_id, res_def_id, + levels, overrides): + res = self.client.put( + '/environments/{0}/{1}/resources/{2}/overrides'.format( + environment_id, + '/'.join(itertools.chain.from_iterable(levels)), + res_def_id + ), + data=overrides + ) + self.assertEqual(res.status_code, 204) + def _assert_db_effect(self, model, key, fields, expected): with self.app.app_context(): obj = model.query.get(key)