Massive update

- new version of python-functionsclient
 - updated swagger doc
 - improved tests
This commit is contained in:
Denis Makogon 2016-12-02 17:11:26 +02:00
parent fee06d71a5
commit e02b003fcb
15 changed files with 353 additions and 68 deletions

View File

@ -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",

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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',