diff --git a/.gitignore b/.gitignore index 125f3ad..f1a9a55 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ python-troveclient.iml # Files created by releasenotes build releasenotes/build .coverage.* +*.json diff --git a/README.md b/README.md index c2ce89a..d28a05d 100644 --- a/README.md +++ b/README.md @@ -123,12 +123,14 @@ In [examples](examples/) folder you can find a script that examines available AP * `LAOS_API_URL` - Project LaOS API endpoint * `OS_AUTH_URL` - OpenStack Auth URL * `OS_PROJECT_ID` - it can be found in OpenStack Dashboard or in CLI - -Along with that, you need to adjust [token_request.json](examples/token_request.json) in order to retrieve X-Auth-Token for further authentication against OpenStack and LaOS API service. +* `OS_USERNAME` - OpenStack project-aligned username +* `OS_PASSWORD` - OpenStack project-aligned user password +* `OS_DOMAIN` - OpenStack project domain name +* `OS_PROJECT_NAME` - OpenStack project name Then just run script: - OS_AUTH_URL=http://192.168.0.112:5000/v3 OS_PROJECT_ID=8fb76785313a4500ac5367eb44a31677 ./hello-lambda.sh + OS_AUTH_URL=http://192.168.0.112:5000/v3 OS_PROJECT_ID=8fb76785313a4500ac5367eb44a31677 OS_USERNAME=admin OS_PASSWORD=root OS_DOMAIN=default OS_PROJECT_NAME=admin ./examples/hello-lambda.sh Please note, that given values are project-specific, so they can't be reused. diff --git a/examples/hello-lambda.sh b/examples/hello-lambda.sh index 4abe688..f21831e 100755 --- a/examples/hello-lambda.sh +++ b/examples/hello-lambda.sh @@ -1,18 +1,47 @@ #!/usr/bin/env bash set +x +set +e export LAOS_API_URL=${LAOS_API_URL:-http://localhost:10001} export OS_AUTH_URL=${OS_AUTH_URL:-http://localhost:5000/v3} +export OS_USERNAME=${OS_USERNAME:-admin} +export OS_PASSOWRD=${OS_PASSWORD:-root} +export OS_DOMAIN=${OS_DOMAIN:-default} export OS_PROJECT_ID=${OS_PROJECT_ID:-"dummy_project_id"} -export OS_TOKEN=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | awk '/X-Subject-Token/ {print $2}'` + + +rm -fr examples/token_request.json +echo -e "{ + \"auth\": { + \"identity\": { + \"methods\": [\"password\"], + \"password\": { + \"user\": { + \"name\": \"${OS_USERNAME:-admin}\", + \"domain\": { \"id\": \"${OS_DOMAIN:-default}\" }, + \"password\": \"${OS_PASSWORD:-root}\" + } + } + }, + \"scope\": { + \"project\": { + \"name\": \"${OS_PROJECT_NAME:-admin}\", + \"domain\": {\"id\": \"${OS_DOMAIN:-default}\" } + } + } + } +}" >> examples/token_request.json + + +export OS_TOKEN=`curl -si -d @examples/token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | awk '/X-Subject-Token/ {print $2}'` echo -e "Listing apps\n" curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Creating app\n" -curl -X POST -d '{"app":{"name": "testapp"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"app":{"name": "testapp"}}' ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Listing apps\n" curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool @@ -20,32 +49,48 @@ curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H " echo -e "Showing app info\n" export raw_app_info=`curl localhost:10001/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool | grep name | awk '{print $2}'` export app_name=${raw_app_info:1:30} -unset $raw_app_info curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name} -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Listing app routes\n" -curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool -echo -e "Creating app sync route\n" -curl -X POST -d '{"route":{"type": "sync", "path": "/hello-sync", "image": "iron/hello"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/route -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" +echo -e "Creating app sync private route\n" +curl -X POST -d '{"route":{"type": "sync", "path": "/hello-sync-private", "image": "iron/hello", "is_public": "false" }}' ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool + +echo -e "Creating app sync public route\n" +curl -X POST -d '{"route":{"type": "sync", "path": "/hello-sync-public", "image": "iron/hello", "is_public": "true" }}' ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Listing app routes\n" -curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool -echo -e "Show app route\n" -curl ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes/hello-sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +echo -e "Show app private route\n" +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool -echo -e "Running app sync route\n" -curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/r/${PROJECT_ID}/${APP_NAME}/hello-sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +echo -e "Show app public route\n" +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-public -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool -echo -e "Deleting app route\n" -curl -X DELETE ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/routes/hello_sync -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +echo -e "Running app sync private route\n" +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/private/${OS_PROJECT_ID}/${app_name}/hello-sync-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool + +echo -e "Running app sync public route\n" +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/public/${app_name}/hello-sync-public -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Creating app async route\n" -curl -X POST -d '{"route":{"type": "async", "path": "/hello-async", "image": "iron/hello"}}' ${LAOS_API_URL}/v1/${PROJECT_ID}/apps/${APP_NAME}/route -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"route":{"type": "async", "path": "/hello-async-private", "image": "iron/hello", "is_public": "false"}}' ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Running app async route\n" -curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/r/${PROJECT_ID}/${APP_NAME}/hello-async -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/private/${OS_PROJECT_ID}/${app_name}/hello-async-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool + +echo -e "Deleting app route\n" +curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-public -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-async-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool + +echo -e "Listing app routes\n" +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Deleting app\n" curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name} -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool + +echo -e "Listing apps\n" +curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool diff --git a/examples/token_request.json b/examples/token_request.json deleted file mode 100644 index 65b9f75..0000000 --- a/examples/token_request.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "auth": { - "identity": { - "methods": ["password"], - "password": { - "user": { - "name": "admin", - "domain": { "id": "default" }, - "password": "root" - } - } - }, - "scope": { - "project": { - "name": "admin", - "domain": { "id": "default" } - } - } - } -} diff --git a/laos/api/controllers/routes.py b/laos/api/controllers/routes.py index 1f6b27a..1a5e81e 100644 --- a/laos/api/controllers/routes.py +++ b/laos/api/controllers/routes.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from aiohttp import web from laos.api.views import app as app_view @@ -34,6 +36,10 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): log, fnclient = c.logger, c.functions_client project_id = request.match_info.get('project_id') app = request.match_info.get('app') + + log.info("Listing app {} routes for project: {}." + .format(app, project_id)) + if not (await app_model.Apps.exists(app, project_id)): return web.json_response(data={ "error": { @@ -44,10 +50,20 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): fn_app_routes = (await (await fnclient.apps.show( app, loop=c.event_loop)).routes.list(loop=c.event_loop)) + for fn_route in fn_app_routes: + stored_route = (await app_model.Routes.find_by( + app_name=app, + project_id=project_id, + path=fn_route.path)).pop() + setattr(fn_route, "is_public", stored_route.public) + + api_url = "{}://{}".format(request.scheme, request.host) log.info("Listing app {} routes for project: {}." .format(app, project_id)) return web.json_response(data={ - "routes": app_view.AppRouteView(fn_app_routes).view(), + "routes": app_view.AppRouteView(api_url, + project_id, + fn_app_routes).view(), "message": "Successfully loaded app routes", }, status=200) @@ -58,6 +74,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): log, fnclient = c.logger, c.functions_client project_id = request.match_info.get('project_id') app = request.match_info.get('app') + if not (await app_model.Apps.exists(app, project_id)): return web.json_response(data={ "error": { @@ -67,6 +84,8 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): data = (await request.json())['route'] path = data['path'] + is_public = json.loads(data.get( + 'is_public', "false")) try: fn_app = await fnclient.apps.show(app, loop=c.event_loop) @@ -95,15 +114,26 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): } }, status=getattr(ex, "status", 500)) - new_fn_route = (await (await fnclient.apps.show( - app, loop=c.event_loop)).routes.create( + new_fn_route = (await fn_app.routes.create( **data, loop=c.event_loop)) + stored_route = await app_model.Routes( + app_name=new_fn_route.appname, + project_id=project_id, + path=new_fn_route.path, + is_public=is_public).save() + log.info("Creating new route in app {} " "for project: {} with data {}" .format(app, project_id, str(data))) + api_url = "{}://{}".format(request.scheme, request.host) + + setattr(new_fn_route, "is_public", stored_route.public) + view = app_view.AppRouteView( + api_url, project_id, [new_fn_route]).view() + return web.json_response(data={ - "route": app_view.AppRouteView([new_fn_route]).view(), + "route": view, "message": "App route successfully created" }, status=200) @@ -116,6 +146,13 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): app = request.match_info.get('app') path = request.match_info.get('route') + 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) route = await fn_app.routes.show( @@ -129,8 +166,20 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): log.info("Requesting route {} in app {} for project: {}" .format(path, app, project_id)) + + 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([route]).view().pop(), + "route": app_view.AppRouteView(api_url, + project_id, + [route]).view().pop(), "message": "App route successfully loaded" }, status=200) @@ -144,10 +193,19 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): path = request.match_info.get('route') 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) await fn_app.routes.delete("/{}".format(path), loop=c.event_loop) + await app_model.Routes.delete(project_id=project_id, app_name=app) except Exception as ex: return web.json_response(data={ "error": { diff --git a/laos/api/controllers/runnable.py b/laos/api/controllers/runnable.py index 3d3dd91..5bbf40a 100644 --- a/laos/api/controllers/runnable.py +++ b/laos/api/controllers/runnable.py @@ -18,14 +18,8 @@ from laos.common.base import controllers from laos.common import config -class RunnableV1Controller(controllers.ServiceControllerBase): +class RunnableMixin(object): - controller_name = "runnable" - # IronFunction uses `r` as runnable instead API version - version = "r" - - @controllers.api_action( - method='POST', route='{project_id}/{app}/{route}') async def run(self, request, **kwargs): c = config.Config.config_instance() fnclient = c.functions_client @@ -68,3 +62,31 @@ class RunnableV1Controller(controllers.ServiceControllerBase): return _data return web.json_response(status=200, data=process_result(result)) + + +class PublicRunnableV1Controller(controllers.ServiceControllerBase, + RunnableMixin): + + controller_name = "public_runnable" + # IronFunction uses `r` as runnable instead API version + version = "r" + + @controllers.api_action( + method='POST', route='{app}/{route}') + async def run(self, request, **kwargs): + return await super(PublicRunnableV1Controller, + self).run(request, **kwargs) + + +class RunnableV1Controller(controllers.ServiceControllerBase, + RunnableMixin): + + controller_name = "runnable" + # IronFunction uses `r` as runnable instead API version + version = "r" + + @controllers.api_action( + method='POST', route='{project_id}/{app}/{route}') + async def run(self, request, **kwargs): + return await super(RunnableV1Controller, + self).run(request, **kwargs) diff --git a/laos/api/controllers/tasks.py b/laos/api/controllers/tasks.py index b7f5d69..2db81b0 100644 --- a/laos/api/controllers/tasks.py +++ b/laos/api/controllers/tasks.py @@ -27,6 +27,12 @@ class TasksV1Controller(controllers.ServiceControllerBase): controller_name = "tasks" version = "v1" + # TODO(denismakogon): + # - define subapp to process requests to tasks API: + # * extract tasks V1 controller to subapp + # - on each request check if route is public our private + # * reject with 401 if route is private + # * accept with 200 if route is public @controllers.api_action( method='GET', route='{project_id}/tasks') async def get(self, request, **kwargs): diff --git a/laos/api/middleware/content_type.py b/laos/api/middleware/content_type.py index 494cfe2..f48f8f6 100644 --- a/laos/api/middleware/content_type.py +++ b/laos/api/middleware/content_type.py @@ -18,12 +18,13 @@ async def content_type_validator(app: web.Application, handler): async def middleware_handler(request: web.Request): headers = request.headers content_type = headers.get("Content-Type") - if "application/json" not in content_type: - return web.json_response( - data={ - "error": { - "message": "Invalid content type" - } - }, status=400) + if request.has_body: + if "application/json" != content_type: + return web.json_response( + data={ + "error": { + "message": "Invalid content type" + } + }, status=400) return await handler(request) return middleware_handler diff --git a/laos/api/views/app.py b/laos/api/views/app.py index 84ac21c..d0e893c 100644 --- a/laos/api/views/app.py +++ b/laos/api/views/app.py @@ -33,16 +33,26 @@ class AppView(object): class AppRouteView(object): - def __init__(self, fn_app_routes): + def __init__(self, api_url, project_id, fn_app_routes): self.routes = fn_app_routes + self.api_url = api_url + self.project_id = project_id def view(self): view = [] for route in self.routes: + if not route.is_public: + path = ("{}/private/{}/{}{}".format( + self.api_url, self.project_id, + route.appname, route.path)) + else: + path = ("{}/public/{}{}".format( + self.api_url, route.appname, route.path)) view.append({ - "path": route.path, + "path": path, "type": route.type, "memory": route.memory, "image": route.image, + "is_public": route.is_public, }) return view diff --git a/laos/common/base/controllers.py b/laos/common/base/controllers.py index d4b34c5..e3ef06d 100644 --- a/laos/common/base/controllers.py +++ b/laos/common/base/controllers.py @@ -35,14 +35,13 @@ class ServiceControllerBase(object): method.arg_method, method.arg_route] for method in methods] - def __init__(self, service: web.Application): + def __init__(self, sub_service: web.Application): for fn, http_method, route in self.__get_handlers(): proxy_fn = '_'.join([fn.__name__, self.controller_name]) setattr(self, proxy_fn, fn) - service.router.add_route(http_method, - "/{}/{}".format(self.version, route), - getattr(self, proxy_fn), - name=proxy_fn) + sub_service.router.add_route( + http_method, "/{}".format(route), + getattr(self, proxy_fn), name=proxy_fn) def api_action(**outter_kwargs): diff --git a/laos/common/base/service.py b/laos/common/base/service.py index c39e720..1ad565e 100644 --- a/laos/common/base/service.py +++ b/laos/common/base/service.py @@ -13,11 +13,8 @@ # under the License. import asyncio -import os -import typing from aiohttp import web -from laos.common.base import controllers as c from laos.common import logger as log @@ -25,49 +22,61 @@ class AbstractWebServer(object): def __init__(self, host: str='127.0.0.1', port: int= '10001', - controllers: typing.List[c.ServiceControllerBase]=None, - middlewares: list=None, + private_controllers: dict=None, + private_middlewares: list=None, + public_middlewares: list=None, + public_controllers: dict=None, event_loop: asyncio.AbstractEventLoop=None, logger=log.UnifiedLogger( log_to_console=True, - level="INFO").setup_logger(__name__)): + level="INFO").setup_logger(__name__), + debug=False): """ HTTP server abstraction class - :param host: - :param port: - :param controllers: - :param middlewares: - :param event_loop: - :param logger: + :param host: Bind host + :param port: Bind port + :param private_controllers: private API controllers mapping + :param private_middlewares: list of private API middleware + :param public_middlewares: + list of public API middleware + :param public_controllers: + public API controllers mapping + :param event_loop: asyncio eventloop + :param logger: logging.Logger """ self.host = host self.port = port - self.controllers = controllers self.event_loop = event_loop - self.service = web.Application( - loop=self.event_loop, - debug=os.environ.get('PYTHONASYNCIODEBUG', 0), - middlewares=middlewares if middlewares else []) - self.service_handler = None - self.server = None self.logger = logger - def _apply_routers(self): - if self.controllers: - for controller in self.controllers: - controller(self.service) + self.root_service = web.Application( + logger=self.logger, + loop=self.event_loop, + debug=debug + ) - def shutdown(self): - self.server.close() - self.event_loop.run_until_complete(self.server.wait_closed()) - self.event_loop.run_until_complete( - self.service_handler.finish_connections(1.0)) - self.event_loop.run_until_complete(self.service.cleanup()) + self.register_subapps(private_controllers, private_middlewares) + self.register_subapps(public_controllers, public_middlewares) + + def _apply_routers(self, service, controllers): + for controller in controllers: + controller(service) + return service + + def register_subapps(self, controllers_mapping: dict, middlewares: list): + if controllers_mapping: + for sub_route, controllers in controllers_mapping.items(): + service = self._apply_routers( + web.Application( + logger=self.logger, + loop=self.event_loop, + middlewares=middlewares + if middlewares else [] + ) + , controllers) + self.root_service.router.add_subapp( + "/{}/".format(sub_route), service) def initialize(self): - self._apply_routers() - try: - web.run_app(self.service, host=self.host, port=self.port, - shutdown_timeout=10, access_log=self.logger) - except KeyboardInterrupt: - self.shutdown() + web.run_app(self.root_service, host=self.host, port=self.port, + shutdown_timeout=10, access_log=self.logger) diff --git a/laos/common/logger.py b/laos/common/logger.py index f3a4c50..ec997b4 100644 --- a/laos/common/logger.py +++ b/laos/common/logger.py @@ -76,7 +76,10 @@ class UnifiedLogger(object, metaclass=utils.Singleton): if 'DEBUG' not in level: self.log_formatter = ( '[%(asctime)s] - ' - '%(message)s') + '%(name)s - ' + '%(module)s.py:%(lineno)d - ' + '%(message)s' + ) else: self. log_formatter = ( '[%(asctime)s] - ' diff --git a/laos/common/persistence.py b/laos/common/persistence.py index 46da76a..8db3c92 100644 --- a/laos/common/persistence.py +++ b/laos/common/persistence.py @@ -27,6 +27,10 @@ class BaseDatabaseModel(object): DELETE = "DELETE FROM {} {}" def __init__(self, **kwargs): + logger = config.Config.config_instance().logger + logger.info("Attempting to create object class instance " + "'{}' with attributes '{}'" + .format(str(self.__class__), str(kwargs))) self.id = uuid.uuid4().hex self.created_at = str(datetime.datetime.now()) self.updated_at = str(datetime.datetime.now()) @@ -34,30 +38,37 @@ class BaseDatabaseModel(object): setattr(self, k, v) async def save(self): + logger = config.Config.config_instance().logger insert = self.INSERT.format( self.table_name, str(tuple([getattr(self, clmn) for clmn in self.columns])) ) - print(insert) + logger.info("Attempting to save object '{}' " + "using SQL query: {}".format(self.table_name, insert)) async with config.Connection.from_class().acquire() as conn: async with conn.cursor() as cur: await cur.execute(insert) await conn.commit() + logger.info("Object saved.") return self @classmethod async def delete(cls, **kwargs): + logger = config.Config.config_instance().logger delete = cls.DELETE.format( cls.table_name, cls.__define_where(**kwargs)) + logger.info("Attempting to delete object '{}' " + "using SQL query: {}".format(cls.table_name, delete)) async with config.Connection.from_class().acquire() as conn: async with conn.cursor() as cur: await cur.execute(delete) await conn.commit() + logger.info("Object gone.") - async def update(self, **kwargs): - async with config.Connection.from_class().acquire() as conn: - async with conn.cursor() as cur: - await cur.execute() + # async def update(self, **kwargs): + # async with config.Connection.from_class().acquire() as conn: + # async with conn.cursor() as cur: + # await cur.execute() @classmethod async def exists(cls, name, project_id): @@ -81,12 +92,15 @@ class BaseDatabaseModel(object): @classmethod async def find_by(cls, **kwargs): + logger = config.Config.config_instance().logger where = cls.__define_where(**kwargs) - + select = cls.SELECT.format( + cls.table_name, where) + logger.info("Attempting to find object(s) '{}' " + "using SQL : {}".format(cls.table_name, select)) async with config.Connection.from_class().acquire() as conn: async with conn.cursor() as cur: - await cur.execute(cls.SELECT.format( - cls.table_name, where)) + await cur.execute(select) results = await cur.fetchall() return [cls.from_tuple(instance) for instance in results] if results else [] diff --git a/laos/models/app.py b/laos/models/app.py index 2547a39..feea424 100644 --- a/laos/models/app.py +++ b/laos/models/app.py @@ -26,3 +26,20 @@ class Apps(persistence.BaseDatabaseModel): "updated_at", "name" ) + + +class Routes(persistence.BaseDatabaseModel): + + table_name = "routes" + columns = ( + "project_id", + "path", + "is_public", + "app_name", + "created_at", + "updated_at", + ) + + @property + def public(self): + return True if self.is_public else False diff --git a/laos/service/laos_api.py b/laos/service/laos_api.py index 794e86b..c455e74 100644 --- a/laos/service/laos_api.py +++ b/laos/service/laos_api.py @@ -35,22 +35,36 @@ class API(service.AbstractWebServer): def __init__(self, host: str='0.0.0.0', port: int=10001, loop: asyncio.AbstractEventLoop=asyncio.get_event_loop(), - logger=None): + logger=None, + debug=False): super(API, self).__init__( host=host, port=port, - controllers=[ - apps.AppV1Controller, - routes.AppRouteV1Controller, - runnable.RunnableV1Controller, - tasks.TasksV1Controller, - ], - middlewares=[ + private_controllers={ + "v1": [ + apps.AppV1Controller, + routes.AppRouteV1Controller, + tasks.TasksV1Controller, + ], + "private": [ + runnable.RunnableV1Controller, + ] + }, + public_controllers={ + "public": [ + runnable.PublicRunnableV1Controller, + ], + }, + private_middlewares=[ keystone.auth_through_token, content_type.content_type_validator, ], + public_middlewares=[ + content_type.content_type_validator, + ], event_loop=loop, logger=logger, + debug=debug, ) @@ -73,6 +87,7 @@ class API(service.AbstractWebServer): help='Logging file') @click.option('--log-file', default=None, help='Log file path') +@click.option('--debug', default=False, is_flag=True) def server(host, port, db_uri, keystone_endpoint, functions_host, @@ -81,6 +96,7 @@ def server(host, port, db_uri, functions_api_protocol, log_level, log_file, + debug, ): """ Starts an Project Laos API service @@ -88,7 +104,7 @@ def server(host, port, db_uri, logger = log.UnifiedLogger( log_to_console=True if not log_file else False, filename=None if not log_file else log_file, - level=log_level).setup_logger(__name__) + level=log_level).setup_logger(__package__) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) loop = asyncio.get_event_loop() @@ -104,17 +120,14 @@ def server(host, port, db_uri, config.Config( auth_url=keystone_endpoint, - functions_host=functions_host, - functions_port=functions_port, - functions_api_protocol=functions_api_protocol, - functions_api_version=functions_api_version, functions_client=fnclient, logger=logger, connection=conn, event_loop=loop, ) - API(host=host, port=port, loop=loop, logger=logger).initialize() + API(host=host, port=port, loop=loop, + logger=logger, debug=debug).initialize() if __name__ == "__main__": diff --git a/laos/tests/__init__.py b/laos/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/migrations/versions/7a2dcf8ac8bf_.py b/migrations/versions/7a2dcf8ac8bf_.py index 044677f..db3a574 100644 --- a/migrations/versions/7a2dcf8ac8bf_.py +++ b/migrations/versions/7a2dcf8ac8bf_.py @@ -27,7 +27,17 @@ def upgrade(): sa.Column('updated_at', sa.String(255)), sa.Column('name', sa.String(255), nullable=False, primary_key=True), ) + op.create_table( + 'routes', + sa.Column('project_id', sa.String(255), nullable=False), + sa.Column('path', sa.String(255), nullable=False, primary_key=True), + sa.Column('is_public', sa.Boolean(create_constraint=False), nullable=False), + sa.Column('app_name', sa.String(255), nullable=False, primary_key=True), + sa.Column('created_at', sa.String(255), nullable=False), + sa.Column('updated_at', sa.String(255), nullable=False), + ) def downgrade(): + op.drop_table('routes') op.drop_table('apps') diff --git a/requirements.txt b/requirements.txt index 3ab138e..df0e2c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,3 @@ alembic>=0.8.4 # MIT click keystoneauth1>=2.14.0 # Apache-2.0 python-keystoneclient==3.6.0 -SQLAlchemy<1.1.0,>=1.0.10 # MIT diff --git a/setup.py b/setup.py index 8bb5eff..09c4692 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ setuptools.setup( "click", "keystoneauth1>=2.14.0", "python-keystoneclient==3.6.0", - "SQLAlchemy<1.1.0,>=1.0.10", ], license='License :: OSI Approved :: Apache Software License', classifiers=[