diff --git a/laos/api/controllers/apps.py b/laos/api/controllers/apps.py index bde58cc..0369e6d 100644 --- a/laos/api/controllers/apps.py +++ b/laos/api/controllers/apps.py @@ -159,22 +159,58 @@ class AppV1Controller(controller.ServiceController): }, status=200 ) - # TODO(denismakogon): disabled until iron-io/functions/pull/259 - # - # @requests.api_action(method='PUT', route='{project_id}/apps/{app}') - # async def update(self, request, **kwargs): - # log = config.Config.config_instance().logger - # project_id = request.match_info.get('project_id') - # app = request.match_info.get('app') - # data = await request.json() - # log.info("Updating an app {} for project: {} with data {}" - # .format(app, project_id, str(data))) - # return web.json_response( - # data={ - # "app": {} - # }, - # status=200 - # ) + + @requests.api_action(method='PUT', route='{project_id}/apps/{app}') + async def update(self, request, **kwargs): + """ + --- + description: Updating project-scoped app + tags: + - Apps + produces: + - application/json + responses: + "200": + description: successful operation. Return "app" JSON + "401": + description: Not authorized. + "404": + description: App not found + """ + project_id = request.match_info.get('project_id') + app_name = request.match_info.get('app') + data = await request.json() + + if not (await app_model.Apps.exists(app_name, project_id)): + return web.json_response(data={ + "error": { + "message": "App {0} not found".format(app_name), + } + }, status=404) + + c = config.Config.config_instance() + fnclient = c.functions_client + try: + fn_app = await fnclient.apps.update( + app_name, loop=c.event_loop, **data) + except Exception as ex: + return web.json_response(data={ + "error": { + "message": getattr(ex, "reason", str(ex)), + } + }, status=getattr(ex, "status", 500)) + + stored_app = (await app_model.Apps.find_by( + project_id=project_id, name=app_name)).pop() + c.logger.info("Updating an app {} for project: {} with data {}" + .format(app_name, project_id, str(data))) + return web.json_response( + data={ + "app": app_view.AppView(stored_app, fn_app).view(), + "message": "App successfully update" + }, + status=200 + ) @requests.api_action(method='DELETE', route='{project_id}/apps/{app}') async def delete(self, request, **kwargs): @@ -217,9 +253,8 @@ class AppV1Controller(controller.ServiceController): await app_model.Apps.delete( project_id=project_id, name=app) - # TODO(denismakogon): enable DELETE to IronFunctions when once - # https://github.com/iron-io/functions/issues/274 implemented - # fn_app = await fnclient.apps.delete(app, loop=c.event_loop) + await fnclient.apps.delete(app, loop=c.event_loop) + return web.json_response( data={ "message": "App successfully deleted", diff --git a/laos/api/controllers/routes.py b/laos/api/controllers/routes.py index 886f55a..f2a3102 100644 --- a/laos/api/controllers/routes.py +++ b/laos/api/controllers/routes.py @@ -78,6 +78,7 @@ class AppRouteV1Controller(controller.ServiceController): return web.json_response(data={ "routes": app_view.AppRouteView(api_url, project_id, + app, fn_app_routes).view(), "message": "Successfully loaded app routes", }, status=200) @@ -166,7 +167,7 @@ class AppRouteV1Controller(controller.ServiceController): **data, loop=c.event_loop)) stored_route = await app_model.Routes( - app_name=new_fn_route.appname, + app_name=app, project_id=project_id, path=new_fn_route.path, is_public=is_public).save() @@ -178,7 +179,7 @@ class AppRouteV1Controller(controller.ServiceController): setattr(new_fn_route, "is_public", stored_route.public) view = app_view.AppRouteView( - api_url, project_id, [new_fn_route]).view_one() + api_url, project_id, app, [new_fn_route]).view_one() return web.json_response(data={ "route": view, @@ -244,10 +245,76 @@ class AppRouteV1Controller(controller.ServiceController): return web.json_response(data={ "route": app_view.AppRouteView(api_url, project_id, + app, [route]).view_one(), "message": "App route successfully loaded" }, status=200) + @requests.api_action( + method='PUT', route='{project_id}/apps/{app}/routes/{route}') + async def update(self, request, **kwargs): + """ + --- + description: Updating project-scoped app route + tags: + - Routes + produces: + - application/json + responses: + "200": + description: Successful operation. Return empty JSON + "401": + description: Not authorized. + "404": + description: App does not exist + "404": + description: App route does not exist + """ + c = config.Config.config_instance() + log, fnclient = c.logger, c.functions_client + project_id = request.match_info.get('project_id') + app = request.match_info.get('app') + path = request.match_info.get('route') + data = await request.json() + log.info("Deleting route {} in app {} for project: {}" + .format(path, app, project_id)) + + if not (await app_model.Apps.exists(app, project_id)): + return web.json_response(data={ + "error": { + "message": "App {0} not found".format(app), + } + }, status=404) + try: + fn_app = await fnclient.apps.show(app, loop=c.event_loop) + await fn_app.routes.show("/{}".format(path), loop=c.event_loop) + route = await fn_app.routes.update( + "/{}".format(path), loop=c.event_loop, **data) + except Exception as ex: + return web.json_response(data={ + "error": { + "message": getattr(ex, "reason", str(ex)), + } + }, status=getattr(ex, "status", 500)) + + api_url = "{}://{}".format(request.scheme, request.host) + + stored_route = (await app_model.Routes.find_by( + app_name=app, + project_id=project_id, + path=route.path)).pop() + + setattr(route, "is_public", stored_route.public) + + return web.json_response(data={ + "route": app_view.AppRouteView(api_url, + project_id, + app, + [route]).view_one(), + + "message": "Route successfully updated", + }, status=200) + @requests.api_action( method='DELETE', route='{project_id}/apps/{app}/routes/{route}') async def delete(self, request, **kwargs): diff --git a/laos/api/views/app.py b/laos/api/views/app.py index 8a61e98..719539c 100644 --- a/laos/api/views/app.py +++ b/laos/api/views/app.py @@ -33,10 +33,11 @@ class AppView(object): class AppRouteView(object): - def __init__(self, api_url, project_id, fn_app_routes): + def __init__(self, api_url, project_id, app_name, fn_app_routes): self.routes = fn_app_routes self.api_url = api_url self.project_id = project_id + self.app_name = app_name def view_one(self): return self.view().pop() @@ -47,15 +48,23 @@ class AppRouteView(object): if not route.is_public: path = ("{}/v1/r/{}/{}{}".format( self.api_url, self.project_id, - route.appname, route.path)) + self.app_name, route.path)) else: path = ("{}/r/{}{}".format( - self.api_url, route.appname, route.path)) - view.append({ + self.api_url, self.app_name, route.path)) + one = { "path": path, "type": route.type, - "memory": route.memory, "image": route.image, "is_public": route.is_public, - }) + } + # temporary solution for + # https://github.com/iron-io/functions/issues/382 + if hasattr(route, "memory"): + one.update(memory=route.memory) + if hasattr(route, "timeout"): + one.update(timeout=route.timeout) + if hasattr(route, "max_concurrency"): + one.update(timeout=route.max_concurrency) + view.append(one) return view diff --git a/laos/tests/common/apps.py b/laos/tests/common/apps.py index 3f5f048..48f7775 100644 --- a/laos/tests/common/apps.py +++ b/laos/tests/common/apps.py @@ -29,8 +29,9 @@ class AppsTestSuite(object): self.assertIn("error", json) def create_and_delete(self): + app = "create_and_delete" create_json, create_status = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + self.test_client.apps.create(app)) delete_json, delete_status = self.testloop.run_until_complete( self.test_client.apps.delete(create_json["app"]["name"])) @@ -42,7 +43,7 @@ class AppsTestSuite(object): self.assertEqual(200, delete_status) def attempt_to_double_create(self): - app = "testapp" + app = "attempt_to_double_create" create_json, _ = self.testloop.run_until_complete( self.test_client.apps.create(app)) err, status = self.testloop.run_until_complete( @@ -60,7 +61,7 @@ class AppsTestSuite(object): self.assertIn("error", json) def delete_with_routes(self): - app_name = "testapp" + app_name = "delete_with_routes" app, _ = self.testloop.run_until_complete( self.test_client.apps.create(app_name)) self.testloop.run_until_complete( @@ -82,3 +83,16 @@ class AppsTestSuite(object): self.assertIn("message", attempt["error"]) self.assertIn("with routes", attempt["error"]["message"]) self.assertEqual(200, status_2) + + def update_app(self): + app_name = "update_app" + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app_name)) + _, update_status = self.testloop.run_until_complete( + self.test_client.apps.update( + app["app"]["name"], config={} + ) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"])) + self.assertEqual(200, update_status) diff --git a/laos/tests/common/base.py b/laos/tests/common/base.py index 924adab..b3b5608 100644 --- a/laos/tests/common/base.py +++ b/laos/tests/common/base.py @@ -23,7 +23,7 @@ class LaosTestsBase(object): def get_loop_and_logger(self, test_type): self.route_data = { - "type": "sync", + "type": "async", "path": "/hello-sync-private", "image": "iron/hello", "is_public": "false" diff --git a/laos/tests/common/client.py b/laos/tests/common/client.py index a9009b4..f3c45a1 100644 --- a/laos/tests/common/client.py +++ b/laos/tests/common/client.py @@ -44,6 +44,10 @@ class AppsV1(object): async def delete(self, app_name): return await self.client.remove(self.app_path, app_name) + async def update(self, app_name, **data): + return await self.client.update( + self.app_path, app_name, **data) + class RoutesV1(object): @@ -51,6 +55,10 @@ class RoutesV1(object): routes_path = "/v1/{}/apps/{}/routes" # /v1/{project_id}/apps/{app}/routes{} route_path = routes_path + "{}" + # /v1/r/{project_id}/{app}{route} + private_execution_path = "/v1/r/{}/{}{}" + # /r/{app}{route} + public_execution_path = "/r/{}{}" def __init__(self, test_client): self.client = test_client @@ -71,6 +79,19 @@ class RoutesV1(object): return await self.client.remove( self.route_path, app_name, path) + async def update(self, app_name, path, **data): + return await self.client.update( + self.route_path, app_name, path, **data) + + async def execute_private(self, app_name, path, **data): + return await self.client.execute( + self.private_execution_path, app_name, path, **data) + + async def execute_public(self, app_name, path, **data): + return await self.client.execute( + self.public_execution_path, app_name, path, + ignore_project_id=True, **data) + class ProjectBoundLaosTestClient(test_utils.TestClient): @@ -113,3 +134,22 @@ class ProjectBoundLaosTestClient(test_utils.TestClient): headers=self.headers) json = await resp.json() return json, resp.status + + async def update(self, route_path, resource_name, *parts, **data): + resp = await self.put( + route_path.format(self.project_id, resource_name, *parts), + data=jsonlib.dumps(data), + headers=self.headers,) + json = await resp.json() + return json, resp.status + + async def execute(self, route, resource_name, *parts, + ignore_project_id=False, **data): + if not ignore_project_id: + route = route.format(self.project_id, resource_name, *parts) + else: + route = route.format(resource_name, *parts) + resp = await self.post( + route, data=jsonlib.dumps(data), headers=self.headers) + json = await resp.json() + return json, resp.status diff --git a/laos/tests/common/routes.py b/laos/tests/common/routes.py index 34197da..89ead7f 100644 --- a/laos/tests/common/routes.py +++ b/laos/tests/common/routes.py @@ -12,9 +12,40 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import json as jsonlib +@contextlib.contextmanager +def setup_execute(self, app_name): + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app_name) + ) + new_app_name = app["app"]["name"] + route, _ = self.testloop.run_until_complete( + self.test_client.routes.create( + new_app_name, **self.route_data) + ) + self.testloop.run_until_complete( + self.test_client.routes.update( + new_app_name, self.route_data["path"], **{ + "type": "sync" + } + ) + ) + try: + yield new_app_name + except Exception as ex: + print(ex) + finally: + self.testloop.run_until_complete( + self.test_client.routes.delete( + new_app_name, self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(new_app_name)) + + class AppRoutesTestSuite(object): def list_routes_from_unknown_app(self): @@ -26,8 +57,9 @@ class AppRoutesTestSuite(object): self.assertIn("not found", json["error"]["message"]) def list_routes_from_existing_app(self): + app = "list_routes_from_existing_app" create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + self.test_client.apps.create(app)) json, status = self.testloop.run_until_complete( self.test_client.routes.list(create_json["app"]["name"]) ) @@ -38,9 +70,10 @@ class AppRoutesTestSuite(object): self.assertIn("message", json) def show_unknown_route_from_existing_app(self): + app = "show_unknown_route_from_existing_app" path = "/unknown_path" create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + self.test_client.apps.create(app)) json, status = self.testloop.run_until_complete( self.test_client.routes.show( create_json["app"]["name"], path) @@ -53,8 +86,9 @@ class AppRoutesTestSuite(object): self.assertIn("not found", json["error"]["message"]) def delete_unknown_route_from_existing_app(self): + app = "delete_unknown_route_from_existing_app" create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + self.test_client.apps.create(app)) json, status = self.testloop.run_until_complete( self.test_client.routes.delete( create_json["app"]["name"], "/unknown_path") @@ -67,18 +101,23 @@ class AppRoutesTestSuite(object): self.assertIn("not found", json["error"]["message"]) def create_and_delete_route(self): - app, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + app_name = "create_and_delete_route" + created, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app_name)) + new_app_name = created["app"]["name"] route, create_status = self.testloop.run_until_complete( self.test_client.routes.create( - app["app"]["name"], **self.route_data) + new_app_name, **self.route_data) ) route_deleted, delete_status = self.testloop.run_until_complete( self.test_client.routes.delete( - app["app"]["name"], self.route_data["path"]) + new_app_name, self.route_data["path"]) ) self.testloop.run_until_complete( - self.test_client.apps.delete(app["app"]["name"])) + self.test_client.apps.delete(new_app_name)) + + print(route) + after_post = route["route"] for k in self.route_data: if k == "path": @@ -94,25 +133,77 @@ class AppRoutesTestSuite(object): self.assertIn("message", route_deleted) def double_create_route(self): - app, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) + app = "double_create_route" + created_app, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app)) + new_app_name = created_app["app"]["name"] + self.testloop.run_until_complete( self.test_client.routes.create( - app["app"]["name"], **self.route_data) + new_app_name, **self.route_data) ) json, double_create_status = self.testloop.run_until_complete( self.test_client.routes.create( - app["app"]["name"], **self.route_data) + new_app_name, **self.route_data) ) self.testloop.run_until_complete( self.test_client.routes.delete( - app["app"]["name"], self.route_data["path"]) + new_app_name, self.route_data["path"]) ) self.testloop.run_until_complete( - self.test_client.apps.delete(app["app"]["name"])) + self.test_client.apps.delete(new_app_name)) self.assertEqual(409, double_create_status) self.assertIn("error", json) self.assertIn("message", json["error"]) self.assertIn("already exist", json["error"]["message"]) + + def update_route(self): + app = "update_route" + created, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app) + ) + new_app_name = created["app"]["name"] + route, _ = self.testloop.run_until_complete( + self.test_client.routes.create( + new_app_name, **self.route_data) + ) + print(route) + updated, update_status = self.testloop.run_until_complete( + self.test_client.routes.update( + new_app_name, self.route_data["path"], **{ + "type": "sync" + } + ) + ) + print(updated) + self.testloop.run_until_complete( + self.test_client.routes.delete( + new_app_name, self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(new_app_name)) + + self.assertEqual(200, update_status) + self.assertNotIn(route["route"]["type"], updated["route"]["type"]) + + def execute_private(self): + with setup_execute(self, "execute_private") as app_name: + result, status = self.testloop.run_until_complete( + self.test_client.routes.execute_private( + app_name, self.route_data["path"] + ) + ) + self.assertIsNotNone(result) + self.assertEqual(200, status) + + def execute_public(self): + with setup_execute(self, "execute_public") as app_name: + result, status = self.testloop.run_until_complete( + self.test_client.routes.execute_public( + app_name, self.route_data["path"] + ) + ) + self.assertIsNotNone(result) + self.assertEqual(200, status) diff --git a/laos/tests/fakes/functions_api.py b/laos/tests/fakes/functions_api.py index 085e22d..1192302 100644 --- a/laos/tests/fakes/functions_api.py +++ b/laos/tests/fakes/functions_api.py @@ -69,18 +69,27 @@ class FakeRoutes(object): ) ) - async def execute(self, path, loop=None): + async def execute(self, path, loop=None, **data): app_routes = APP_ROUTES[self.app_name] if path not in [route.path for route in app_routes]: raise client.FunctionsAPIException( "App {} route {} not found.".format( self.app_name, path), 404) else: - route = await self.show(path) + route = await self.show(path, loop=loop) return "Hello world!" if route.type == "sync" else { "call_id": uuid.uuid4().hex } + async def update(self, route_path, loop=None, **data): + route = await self.show(route_path, loop=loop) + if "path" in data: + del data['path'] + route.__kwargs__.update(data) + for k, v in route.__kwargs__.items(): + setattr(route, k, v) + return route + class FakeApps(object): @@ -104,17 +113,29 @@ class FakeApps(object): async def show(self, app_name, loop=None): if app_name not in APPS: raise client.FunctionsAPIException( - "App {} already exist.".format(app_name), 404) + "App {} not found.".format(app_name), 404) else: return APPS.get(app_name) async def delete(self, app_name, loop=None): if app_name not in APPS: raise client.FunctionsAPIException( - "App {} already exist.".format(app_name), 404) + "App {} not exist.".format(app_name), 404) else: + if APP_ROUTES[app_name]: + raise client.FunctionsAPIException( + "Cannot remove apps with routes", 403) del APPS[app_name] + async def update(self, app_name, loop=None, **data): + app = await self.show(app_name, loop=loop) + if 'name' in data: + del data['name'] + app.__kwargs__.update(data) + for k, v in app.__kwargs__.items(): + setattr(app, k, v) + return app + class FunctionsAPIV1(object): diff --git a/laos/tests/functional/base.py b/laos/tests/functional/base.py index 9bb28aa..f596087 100644 --- a/laos/tests/functional/base.py +++ b/laos/tests/functional/base.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import os import testtools import uuid @@ -78,20 +77,9 @@ class LaosFunctionalTestsBase(base.LaosTestsBase, testtools.TestCase): self.test_client = client.ProjectBoundLaosTestClient( self.testapp, self.project_id) - self.route_data = { - "type": "sync", - "path": "/hello-sync-private", - "image": "iron/hello", - "is_public": "false" - } - self.testloop.run_until_complete(self.test_client.start_server()) super(LaosFunctionalTestsBase, self).setUp() def tearDown(self): - functions_api.APPS = {} - functions_api.ROUTES = collections.defaultdict(list) - # ^ temporary solution, - # until https://github.com/iron-io/functions/issues/274 fixed self.testloop.run_until_complete(self.test_client.close()) super(LaosFunctionalTestsBase, self).tearDown() diff --git a/laos/tests/functional/test_apps.py b/laos/tests/functional/test_apps.py index adc5038..9035b9d 100644 --- a/laos/tests/functional/test_apps.py +++ b/laos/tests/functional/test_apps.py @@ -35,3 +35,6 @@ class TestApps(base.LaosFunctionalTestsBase, apps_suite.AppsTestSuite): def test_delete_with_routes(self): super(TestApps, self).delete_with_routes() + + def test_update_app(self): + super(TestApps, self).update_app() diff --git a/laos/tests/functional/test_routes.py b/laos/tests/functional/test_routes.py index 2544d93..2bef5fb 100644 --- a/laos/tests/functional/test_routes.py +++ b/laos/tests/functional/test_routes.py @@ -40,3 +40,12 @@ class TestAppRoutes(base.LaosFunctionalTestsBase, def test_double_create_route(self): super(TestAppRoutes, self).double_create_route() + + def test_update_route(self): + super(TestAppRoutes, self).update_route() + + def test_private_execution(self): + super(TestAppRoutes, self).execute_private() + + def test_public_execution(self): + super(TestAppRoutes, self).execute_private() diff --git a/laos/tests/integration/test_apps.py b/laos/tests/integration/test_apps.py index 09ee9ac..0fac0e8 100644 --- a/laos/tests/integration/test_apps.py +++ b/laos/tests/integration/test_apps.py @@ -36,3 +36,6 @@ class TestIntegrationApps(base.LaosIntegrationTestsBase, def test_delete_with_routes(self): super(TestIntegrationApps, self).delete_with_routes() + + def test_update_app(self): + super(TestIntegrationApps, self).update_app() diff --git a/laos/tests/integration/test_routes.py b/laos/tests/integration/test_routes.py index d14e2ed..21ad473 100644 --- a/laos/tests/integration/test_routes.py +++ b/laos/tests/integration/test_routes.py @@ -45,6 +45,13 @@ class TestIntegrationAppRoutes(base.LaosFunctionalTestsBase, ).create_and_delete_route() def test_double_create_route(self): - super( - TestIntegrationAppRoutes, self - ).double_create_route() + super(TestIntegrationAppRoutes, self).double_create_route() + + def test_update_route(self): + super(TestIntegrationAppRoutes, self).update_route() + + def test_private_execution(self): + super(TestIntegrationAppRoutes, self).execute_private() + + def test_public_execution(self): + super(TestIntegrationAppRoutes, self).execute_private() diff --git a/requirements.txt b/requirements.txt index 865e4aa..185b8a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ alembic==0.8.8 # MIT click==6.6 # Apache-2.0 # IronFunctions -python-functionsclient==0.0.1 +python-functionsclient==0.0.2 # OpenStack keystoneauth1==2.15.0 # Apache-2.0 diff --git a/setup.py b/setup.py index 6a27e37..9f716cf 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setuptools.setup( "aiomysql==0.0.9", "alembic==0.8.8", "click==6.6", - "python-functionsclient==0.0.1", + "python-functionsclient==0.0.2", "keystoneauth1==2.15.0", "python-keystoneclient==3.6.0", "aiohttp-swagger==1.0.2", @@ -46,8 +46,6 @@ setuptools.setup( 'Intended Audience :: System Administrators', 'Intended Audience :: Developers', 'Environment :: No Input/Output (Daemon)', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: ' 'Libraries :: Python Modules',