From 19cd85e8188ec841f8ecdfa4ee77aba335947516 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Sat, 28 Apr 2018 11:55:35 +1200 Subject: [PATCH] Functional test for function version detach/get - Only admin user can detach function version. - Admin user has read access to user's function version. Change-Id: I345a2aa17d7131984038f99cdd6e6f246bec1d24 Story: 2001829 Task: 14456 --- qinling/api/controllers/v1/function.py | 6 ++ .../api/controllers/v1/function_version.py | 16 +++- qinling/db/sqlalchemy/api.py | 14 +++- .../services/qinling_client.py | 45 +++++++++-- .../tests/api/test_function_versions.py | 79 +++++++++++++++++++ .../tests/api/test_functions.py | 24 ++++-- 6 files changed, 167 insertions(+), 17 deletions(-) diff --git a/qinling/api/controllers/v1/function.py b/qinling/api/controllers/v1/function.py index 497715a7..ba2c35f0 100644 --- a/qinling/api/controllers/v1/function.py +++ b/qinling/api/controllers/v1/function.py @@ -100,7 +100,13 @@ class FunctionsController(rest.RestController): @rest_utils.wrap_pecan_controller_exception @pecan.expose() + @pecan.expose('json') def get(self, id): + """Get function information or download function package. + + This method can support HTTP request using either + 'Accept:application/json' or no 'Accept' header. + """ LOG.info("Get function %s.", id) download = strutils.bool_from_string( diff --git a/qinling/api/controllers/v1/function_version.py b/qinling/api/controllers/v1/function_version.py index efed7218..cab53557 100644 --- a/qinling/api/controllers/v1/function_version.py +++ b/qinling/api/controllers/v1/function_version.py @@ -65,7 +65,7 @@ class FunctionVersionsController(rest.RestController): with db_api.transaction(): # Get latest function package md5 and version number - func_db = db_api.get_function(function_id) + func_db = db_api.get_function(function_id, insecure=False) if func_db.code['source'] != constants.PACKAGE_FUNCTION: raise exc.NotAllowedException( "Function versioning only allowed for %s type " @@ -140,8 +140,12 @@ class FunctionVersionsController(rest.RestController): @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(resources.FunctionVersions, types.uuid) def get_all(self, function_id): + """Get all the versions of the given function. + + Admin user can get all versions for the normal user's function. + """ acl.enforce('function_version:get_all', context.get_ctx()) - LOG.info("Getting function versions for function %s.", function_id) + LOG.info("Getting versions for function %s.", function_id) # Getting function and versions needs to happen in a db transaction with db_api.transaction(): @@ -155,7 +159,13 @@ class FunctionVersionsController(rest.RestController): @rest_utils.wrap_pecan_controller_exception @pecan.expose() + @pecan.expose('json') def get(self, function_id, version): + """Get function version or download function version package. + + This method can support HTTP request using either + 'Accept:application/json' or no 'Accept' header. + """ ctx = context.get_ctx() acl.enforce('function_version:get', ctx) @@ -175,7 +185,7 @@ class FunctionVersionsController(rest.RestController): LOG.info("Downloading version %s for function %s.", version, function_id) - f = self.storage_provider.retrieve(ctx.projectid, function_id, + f = self.storage_provider.retrieve(version_db.project_id, function_id, None, version=version) if isinstance(f, collections.Iterable): diff --git a/qinling/db/sqlalchemy/api.py b/qinling/db/sqlalchemy/api.py index 82740e8b..9470dd06 100644 --- a/qinling/db/sqlalchemy/api.py +++ b/qinling/db/sqlalchemy/api.py @@ -516,10 +516,18 @@ def increase_function_version(function_id, old_version, session=None, return version +@db_base.insecure_aware() @db_base.session_aware() -def get_function_version(function_id, version, session=None): - version_db = _secure_query(models.FunctionVersion).filter_by( - function_id=function_id, version_number=version).first() +def get_function_version(function_id, version, session=None, insecure=None): + if insecure: + query = db_base.model_query(models.FunctionVersion) + else: + query = _secure_query(models.FunctionVersion) + + version_db = query.filter_by( + function_id=function_id, version_number=version + ).first() + if not version_db: raise exc.DBEntityNotFoundError( "FunctionVersion not found [function_id=%s, version_number=%s]" % diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py index 08f8509e..4d1804c9 100644 --- a/qinling_tempest_plugin/services/qinling_client.py +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -106,14 +106,25 @@ class QinlingClient(client_base.QinlingClientBase): return resp, json.loads(resp.text) + def get_function(self, function_id): + resp, body = self.get( + '/v1/functions/{id}'.format(id=function_id), + ) + + return resp, json.loads(body) + def download_function(self, function_id): return self.get('/v1/functions/%s?download=true' % function_id, headers={}) - def detach_function(self, function_id): - return self.post('/v1/functions/%s/detach' % function_id, - None, - headers={}) + def detach_function(self, function_id, version=0): + if version == 0: + url = '/v1/functions/%s/detach' % function_id + else: + url = '/v1/functions/%s/versions/%s/detach' % \ + (function_id, version) + + return self.post(url, None, headers={}) def create_execution(self, function_id, input=None, sync=True, version=0): req_body = { @@ -130,8 +141,16 @@ class QinlingClient(client_base.QinlingClientBase): return self.get('/v1/executions/%s/log' % execution_id, headers={'Accept': 'text/plain'}) - def get_function_workers(self, function_id): - return self.get_resources('functions/%s/workers' % function_id) + def get_function_workers(self, function_id, version=0): + q_params = None + if version > 0: + q_params = "/?function_version=%s" % version + + url = 'functions/%s/workers' % function_id + if q_params: + url += q_params + + return self.get_resources(url) def create_webhook(self, function_id, version=0): req_body = {"function_id": function_id, "function_version": version} @@ -174,3 +193,17 @@ class QinlingClient(client_base.QinlingClientBase): pass else: raise + + def get_function_version(self, function_id, version): + resp, body = self.get( + '/v1/functions/%s/versions/%s' % (function_id, version), + ) + + return resp, json.loads(body) + + def get_function_versions(self, function_id): + resp, body = self.get( + '/v1/functions/%s/versions' % (function_id), + ) + + return resp, json.loads(body) diff --git a/qinling_tempest_plugin/tests/api/test_function_versions.py b/qinling_tempest_plugin/tests/api/test_function_versions.py index 5e373fc6..161a62ec 100644 --- a/qinling_tempest_plugin/tests/api/test_function_versions.py +++ b/qinling_tempest_plugin/tests/api/test_function_versions.py @@ -13,6 +13,7 @@ # limitations under the License. from tempest.lib import decorators from tempest.lib import exceptions +import tenacity from qinling_tempest_plugin.tests import base @@ -70,3 +71,81 @@ class FunctionVersionsTest(base.BaseQinlingTest): numbers = [v['version_number'] for v in body['function_versions']] self.assertIn(version_1, numbers) self.assertIn(version_2, numbers) + + @decorators.idempotent_id('3f735ed4-64b0-4ec3-8bf2-507e38dcea19') + def test_create_admin_not_allowed(self): + """test_create_admin_not_allowed + + Even admin user can not create function version for normal user's + function. + """ + function_id = self.create_function() + + self.assertRaises( + exceptions.NotFound, + self.admin_client.create_function_version, + function_id + ) + + @decorators.idempotent_id('43c06f41-d116-43a7-a61c-115f7591b22e') + def test_get_by_admin(self): + """Admin user can get normal user's function version.""" + function_id = self.create_function() + version = self.create_function_version(function_id) + + resp, body = self.admin_client.get_function_version(function_id, + version) + + self.assertEqual(200, resp.status) + self.assertEqual(version, body.get("version_number")) + + @decorators.idempotent_id('e6b865d8-ffa8-4cfc-8afb-820c64f9b2af') + def test_get_all_by_admin(self): + """Admin user can list normal user's function version.""" + function_id = self.create_function() + version = self.create_function_version(function_id) + + resp, body = self.admin_client.get_function_versions(function_id) + + self.assertEqual(200, resp.status) + self.assertIn( + version, + [v['version_number'] for v in body['function_versions']] + ) + + @decorators.idempotent_id('7898f89f-a490-42a3-8cf7-63cbd9543a06') + def test_detach(self): + """Admin only operation.""" + function_id = self.create_function() + version = self.create_function_version(function_id) + + # Create execution to allocate worker + resp, _ = self.client.create_execution( + function_id, input='{"name": "Qinling"}', version=version + ) + self.assertEqual(201, resp.status) + + resp, body = self.admin_client.get_function_workers(function_id, + version=version) + self.assertEqual(200, resp.status) + self.assertEqual(1, len(body['workers'])) + + # Detach function version from workers + resp, _ = self.admin_client.detach_function(function_id, + version=version) + self.assertEqual(202, resp.status) + + def _assert_workers(): + resp, body = self.admin_client.get_function_workers( + function_id, + version=version + ) + self.assertEqual(200, resp.status) + self.assertEqual(0, len(body['workers'])) + + r = tenacity.Retrying( + wait=tenacity.wait_fixed(1), + stop=tenacity.stop_after_attempt(5), + retry=tenacity.retry_if_exception_type(AssertionError) + ) + r.call(_assert_workers) diff --git a/qinling_tempest_plugin/tests/api/test_functions.py b/qinling_tempest_plugin/tests/api/test_functions.py index 30c5501a..d25deb93 100644 --- a/qinling_tempest_plugin/tests/api/test_functions.py +++ b/qinling_tempest_plugin/tests/api/test_functions.py @@ -68,12 +68,27 @@ class FunctionsTest(base.BaseQinlingTest): self.assertEqual(400, resp.status_code) - @decorators.idempotent_id('051f3106-df01-4fcd-a0a3-c81c99653163') - def test_get_all_admin(self): - # Create function by normal user + @decorators.idempotent_id('f8dde7fc-fbcc-495c-9b39-70666b7d3f64') + def test_get_by_admin(self): + """test_get_by_admin + + Admin user can get the function by directly specifying the function id. + """ + function_id = self.create_function(self.python_zip_file) + + resp, body = self.admin_client.get_function(function_id) + + self.assertEqual(200, resp.status) + self.assertEqual(function_id, body['id']) + + @decorators.idempotent_id('051f3106-df01-4fcd-a0a3-c81c99653163') + def test_get_all_admin(self): + """test_get_all_admin + + Admin user needs to specify filters to get all the functions. + """ function_id = self.create_function(self.python_zip_file) - # Get functions by admin resp, body = self.admin_client.get_resources('functions') self.assertEqual(200, resp.status) @@ -82,7 +97,6 @@ class FunctionsTest(base.BaseQinlingTest): [function['id'] for function in body['functions']] ) - # Get other projects functions by admin resp, body = self.admin_client.get_resources( 'functions?all_projects=true' )