diff --git a/qinling/api/controllers/v1/execution.py b/qinling/api/controllers/v1/execution.py index 55dde2ba..644a328e 100644 --- a/qinling/api/controllers/v1/execution.py +++ b/qinling/api/controllers/v1/execution.py @@ -23,6 +23,7 @@ from qinling.api.controllers.v1 import resources from qinling.api.controllers.v1 import types from qinling import context from qinling.db import api as db_api +from qinling import exceptions as exc from qinling import rpc from qinling.utils import executions from qinling.utils import rest_utils @@ -60,6 +61,11 @@ class ExecutionsController(rest.RestController): acl.enforce('execution:create', ctx) params = body.to_dict() + if not (params.get("function_id") or params.get("function_alias")): + raise exc.InputException( + 'Either function_alias or function_id must be provided.' + ) + LOG.info("Creating %s. [params=%s]", self.type, params) db_model = executions.create_execution(self.engine_client, params) diff --git a/qinling/api/controllers/v1/resources.py b/qinling/api/controllers/v1/resources.py index 8f84f333..49a39809 100644 --- a/qinling/api/controllers/v1/resources.py +++ b/qinling/api/controllers/v1/resources.py @@ -294,8 +294,9 @@ class RuntimePool(Resource): class Execution(Resource): id = types.uuid - function_id = wsme.wsattr(types.uuid, mandatory=True) + function_id = wsme.wsattr(types.uuid) function_version = wsme.wsattr(int, default=0) + function_alias = wtypes.text description = wtypes.text status = wsme.wsattr(wtypes.text, readonly=True) sync = bool diff --git a/qinling/tests/unit/api/controllers/v1/test_execution.py b/qinling/tests/unit/api/controllers/v1/test_execution.py index 8369182d..a767c910 100644 --- a/qinling/tests/unit/api/controllers/v1/test_execution.py +++ b/qinling/tests/unit/api/controllers/v1/test_execution.py @@ -58,6 +58,39 @@ class TestExecutionController(base.APITest): resp = self.app.get('/v1/functions/%s/versions/1' % self.func_id) self.assertEqual(1, resp.json.get('count')) + @mock.patch('qinling.rpc.EngineClient.create_execution') + def test_post_with_alias(self, mock_rpc): + db_api.increase_function_version(self.func_id, 0, + description="version 1") + name = self.rand_name(name="alias", prefix=self.prefix) + body = { + 'function_id': self.func_id, + 'function_version': 1, + 'name': name + } + db_api.create_function_alias(**body) + + execution_body = { + 'function_alias': name + } + resp = self.app.post_json('/v1/executions', execution_body) + self.assertEqual(201, resp.status_int) + + resp = self.app.get('/v1/functions/%s' % self.func_id) + self.assertEqual(0, resp.json.get('count')) + + resp = self.app.get('/v1/functions/%s/versions/1' % self.func_id) + self.assertEqual(1, resp.json.get('count')) + + def test_post_without_required_params(self): + resp = self.app.post( + '/v1/executions', + params={}, + expect_errors=True + ) + + self.assertEqual(400, resp.status_int) + @mock.patch('qinling.rpc.EngineClient.create_execution') def test_post_rpc_error(self, mock_create_execution): mock_create_execution.side_effect = exc.QinlingException diff --git a/qinling/utils/executions.py b/qinling/utils/executions.py index 2c9c9718..207fde51 100644 --- a/qinling/utils/executions.py +++ b/qinling/utils/executions.py @@ -70,10 +70,18 @@ def _update_function_version_db(version_id, pre_count): def create_execution(engine_client, params): - function_id = params['function_id'] + function_alias = params.get('function_alias') + function_id = params.get('function_id') + version = params.get('function_version', 0) is_sync = params.get('sync', True) input = params.get('input') - version = params.get('function_version', 0) + + if function_alias: + alias_db = db_api.get_function_alias(function_alias) + function_id = alias_db.function_id + version = alias_db.function_version + params.update({'function_id': function_id, + 'version': version}) func_db = db_api.get_function(function_id) runtime_id = func_db.runtime_id diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py index d5641b17..c1eefc27 100644 --- a/qinling_tempest_plugin/services/qinling_client.py +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -127,13 +127,29 @@ class QinlingClient(client_base.QinlingClientBase): return self.post(url, None, headers={}) - def create_execution(self, function_id, input=None, sync=True, version=0): - req_body = { - 'function_id': function_id, - 'function_version': version, - 'sync': sync, - 'input': input - } + def create_execution(self, function_id=None, alias_name=None, input=None, + sync=True, version=0): + """Create execution. + + alias_name takes precedence over function_id. + """ + if alias_name: + req_body = { + 'function_alias': alias_name, + 'sync': sync, + 'input': input + } + elif function_id: + req_body = { + 'function_id': function_id, + 'function_version': version, + 'sync': sync, + 'input': input + } + else: + raise Exception("Either alias_name or function_id must be " + "provided.") + resp, body = self.post_json('executions', req_body) return resp, body @@ -208,3 +224,46 @@ class QinlingClient(client_base.QinlingClientBase): ) return resp, json.loads(body) + + def create_function_alias(self, name, function_id, + function_version=0, description=None): + req_body = { + 'function_id': function_id, + 'function_version': function_version, + 'name': name + } + if description is not None: + req_body['description'] = description + + resp, body = self.post_json('/aliases', req_body) + + return resp, body + + def delete_function_alias(self, alias_name, ignore_notfound=False): + try: + resp, _ = self.delete('/v1/aliases/%s' % alias_name) + return resp + except exceptions.NotFound: + if ignore_notfound: + pass + else: + raise + + def get_function_alias(self, alias_name): + resp, body = self.get('/v1/aliases/%s' % alias_name) + + return resp, json.loads(body) + + def update_function_alias(self, alias_name, function_id=None, + function_version=None, description=None): + req_body = {} + if function_id is not None: + req_body['function_id'] = function_id + if function_version is not None: + req_body['function_version'] = function_version + if description is not None: + req_body['description'] = description + + resp, body = self.put_json('/v1/aliases/%s' % alias_name, req_body) + + return resp, body diff --git a/qinling_tempest_plugin/tests/api/test_executions.py b/qinling_tempest_plugin/tests/api/test_executions.py index 2f61ca94..174df339 100644 --- a/qinling_tempest_plugin/tests/api/test_executions.py +++ b/qinling_tempest_plugin/tests/api/test_executions.py @@ -121,6 +121,32 @@ class ExecutionsTest(base.BaseQinlingTest): self.assertEqual(200, resp.status) self.assertNotIn('Hello, World', body) + @decorators.idempotent_id('dbf4bd84-bde3-4d1d-8dec-93aaf18b4b5f') + def test_create_with_function_alias(self): + function_id = self.create_function() + + alias_name = self.create_function_alias(function_id) + execution_id = self.create_execution(alias_name=alias_name) + resp, body = self.client.get_execution_log(execution_id) + self.assertEqual(200, resp.status) + self.assertIn('Hello, World', body) + + version_1 = self.create_function_version(function_id) + alias_name_1 = self.create_function_alias(function_id, version_1) + execution_id = self.create_execution(alias_name=alias_name_1) + resp, body = self.client.get_execution_log(execution_id) + self.assertEqual(200, resp.status) + self.assertIn('Hello, World', body) + + self.update_function_package(function_id, + "python/test_python_sleep.py") + version_2 = self.create_function_version(function_id) + alias_name_2 = self.create_function_alias(function_id, version_2) + execution_id = self.create_execution(alias_name=alias_name_2) + resp, body = self.client.get_execution_log(execution_id) + self.assertEqual(200, resp.status) + self.assertNotIn('Hello, World', body) + @decorators.idempotent_id('8096cc52-64d2-4660-a657-9ac0bdd743ae') def test_execution_async(self): function_id = self.create_function() diff --git a/qinling_tempest_plugin/tests/base.py b/qinling_tempest_plugin/tests/base.py index 5cc77b13..e76fa23d 100644 --- a/qinling_tempest_plugin/tests/base.py +++ b/qinling_tempest_plugin/tests/base.py @@ -213,9 +213,15 @@ class BaseQinlingTest(test.BaseTestCase): return version - def create_execution(self, function_id, version=0, input=None): - resp, body = self.client.create_execution(function_id, version=version, - input=input) + def create_execution(self, function_id=None, alias_name=None, version=0, + input=None): + if alias_name: + resp, body = self.client.create_execution(alias_name=alias_name, + input=input) + else: + resp, body = self.client.create_execution(function_id, + version=version, + input=input) self.assertEqual(201, resp.status) @@ -226,3 +232,20 @@ class BaseQinlingTest(test.BaseTestCase): self.assertEqual('success', body['status']) return execution_id + + def create_function_alias(self, function_id=None, function_version=0): + name = data_utils.rand_name(name="alias", prefix=self.name_prefix) + if not function_id: + function_id = self.create_function() + + resp, body = self.client.create_function_alias(name, + function_id, + function_version) + + self.assertEqual(201, resp.status) + + alias_name = body['name'] + self.addCleanup(self.client.delete_function_alias, alias_name, + ignore_notfound=True) + + return alias_name