Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/browser/app.py: 91.30%

46 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-10-04 17:48 +0000

1# Copyright (C) 2021 Bloomberg LP 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# <http://www.apache.org/licenses/LICENSE-2.0> 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15import os 

16import re 

17from typing import TYPE_CHECKING, Awaitable, Callable, Optional, cast 

18 

19from aiohttp import web 

20 

21# TODO Possible upstream fix needed. Module "aiohttp_middlewares" does not explicitly export attribute 

22# Possibly fixed with dependency update from 1.2.1 to 2.2.0 

23from aiohttp_middlewares import cors_middleware # type: ignore[attr-defined] 

24from aiohttp_middlewares.annotations import Handler, Middleware, UrlCollection 

25 

26from buildgrid.server.browser import rest_api, utils 

27from buildgrid.server.browser.utils import ResponseCache 

28 

29if TYPE_CHECKING: 

30 from buildgrid.server.app.cli import Context 

31 

32 

33def serve_client(base_path: str) -> Callable[[web.Request], Awaitable[web.FileResponse]]: 

34 async def _serve_client(request: web.Request) -> web.FileResponse: 

35 if not request.match_info["path"]: 

36 path = os.path.join(base_path, "index.html") 

37 else: 

38 path = os.path.join(base_path, request.match_info["path"]) 

39 if not os.path.exists(path): 

40 path = os.path.join(base_path, "index.html") 

41 return web.FileResponse(path) 

42 

43 return _serve_client 

44 

45 

46def cors_middleware_with_error(origins: UrlCollection, urls: UrlCollection, allow_all: bool = False) -> Middleware: 

47 assert origins is not None 

48 assert urls is not None 

49 middleware = cors_middleware(allow_all=allow_all, origins=origins, urls=urls) 

50 

51 @web.middleware 

52 async def _middleware(request: web.Request, handler: Handler) -> web.StreamResponse: 

53 origin = request.headers.get("Origin") 

54 headers = utils.get_cors_headers(origin, origins, allow_all) 

55 try: 

56 return cast(web.StreamResponse, await middleware(request, handler)) 

57 

58 except web.HTTPNotFound: 

59 raise web.HTTPNotFound(headers=headers) 

60 

61 except web.HTTPBadRequest: 

62 raise web.HTTPBadRequest(headers=headers) 

63 

64 except web.HTTPInternalServerError: 

65 raise web.HTTPInternalServerError(headers=headers) 

66 

67 return _middleware 

68 

69 

70def create_app( 

71 context: "Context", 

72 cache: ResponseCache, 

73 cors_origins: UrlCollection, 

74 static_path: Optional[str] = None, 

75 allow_cancelling_operations: bool = False, 

76 tarball_dir: Optional[str] = None, 

77) -> web.Application: 

78 allow_all = len(cors_origins) == 0 

79 app = web.Application( 

80 middlewares=( 

81 cors_middleware_with_error(allow_all=allow_all, origins=cors_origins, urls=[re.compile(r"^\/api")]), 

82 ) 

83 ) 

84 routes = [ 

85 web.get("/api/v1/build_events", rest_api.query_build_events_handler(context)), 

86 web.get("/api/v1/operation_filters", rest_api.get_operation_filters_handler), 

87 web.get("/api/v1/operations", rest_api.list_operations_handler(context, cache)), 

88 web.get("/api/v1/operations/{name}", rest_api.get_operation_handler(context, cache)), 

89 web.get( 

90 "/api/v1/operations/{name}/request_metadata", rest_api.get_operation_request_metadata_handler(context) 

91 ), 

92 web.get("/api/v1/operations/{name}/client_identity", rest_api.get_operation_client_identity_handler(context)), 

93 web.get("/api/v1/action_results/{hash}/{size_bytes}", rest_api.get_action_result_handler(context, cache)), 

94 web.get( 

95 "/api/v1/blobs/{hash}/{size_bytes}", 

96 rest_api.get_blob_handler(context, cache, allow_all=allow_all, allowed_origins=cors_origins), 

97 ), 

98 web.get( 

99 "/api/v1/tarballs/{hash}_{size_bytes}.tar.gz", 

100 rest_api.get_tarball_handler( 

101 context, cache, allow_all=allow_all, allowed_origins=cors_origins, tarball_dir=tarball_dir 

102 ), 

103 ), 

104 web.get("/ws/logstream", rest_api.logstream_handler(context)), 

105 ] 

106 

107 if allow_cancelling_operations: 

108 routes.append(web.delete("/api/v1/operations/{name}", rest_api.cancel_operation_handler(context))) 

109 

110 if static_path is not None: 

111 routes.append(web.get("/{path:.*}", serve_client(static_path))) 

112 app.add_routes(routes) 

113 return app