Skip to content

Commit abab42d

Browse files
dbutenhofportante
authored andcommitted
CORS fixes and a new API
1 parent e4ddee5 commit abab42d

File tree

7 files changed

+250
-47
lines changed

7 files changed

+250
-47
lines changed

lib/pbench/server/api/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from flask import Flask
1010
from flask_restful import Api
11+
from flask_cors import CORS
1112

1213
from pbench.server import PbenchServerConfig
1314
from pbench.common.exceptions import BadConfig, ConfigFileNotSpecified
@@ -16,6 +17,7 @@
1617
from pbench.common.logger import get_pbench_logger
1718
from pbench.server.api.resources.query_apis.elasticsearch_api import Elasticsearch
1819
from pbench.server.api.resources.query_apis.query_controllers import QueryControllers
20+
from pbench.server.api.resources.query_apis.query_month_indices import QueryMonthIndices
1921

2022

2123
def register_endpoints(api, app, config):
@@ -41,12 +43,16 @@ def register_endpoints(api, app, config):
4143
api.add_resource(
4244
GraphQL, f"{base_uri}/graphql", resource_class_args=(config, app.logger),
4345
)
44-
4546
api.add_resource(
4647
QueryControllers,
4748
f"{base_uri}/controllers/list",
4849
resource_class_args=(config, app.logger),
4950
)
51+
api.add_resource(
52+
QueryMonthIndices,
53+
f"{base_uri}/controllers/months",
54+
resource_class_args=(config, app.logger),
55+
)
5056

5157

5258
def get_server_config():
@@ -69,6 +75,7 @@ def create_app(server_config):
6975

7076
app = Flask("api-server")
7177
api = Api(app)
78+
CORS(app, resources={r"/api/*": {"origins": "*"}})
7279

7380
app.logger = get_pbench_logger(__name__, server_config)
7481

lib/pbench/server/api/resources/graphql_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import requests
22
from flask_restful import Resource, abort
33
from flask import request, make_response
4-
from flask_cors import cross_origin
54

65

76
class GraphQL(Resource):
@@ -12,7 +11,6 @@ def __init__(self, config, logger):
1211
self.graphql_host = config.get_conf(__name__, "graphql", "host", self.logger)
1312
self.graphql_port = config.get_conf(__name__, "graphql", "port", self.logger)
1413

15-
@cross_origin()
1614
def post(self):
1715
self.graphql = f"http://{self.graphql_host}:{self.graphql_port}"
1816

@@ -27,6 +25,9 @@ def post(self):
2725
# query GraphQL
2826
gql_response = requests.post(self.graphql, json=json_data)
2927
gql_response.raise_for_status()
28+
except requests.exceptions.HTTPError as e:
29+
self.logger.exception("HTTP error {} from Elasticsearch post request", e)
30+
abort(gql_response.status_code, message=f"HTTP error {e} from GraphQL")
3031
except requests.exceptions.ConnectionError:
3132
self.logger.exception("Connection refused during the GraphQL post request")
3233
abort(502, message="Network problem, could not post to GraphQL Endpoint")

lib/pbench/server/api/resources/query_apis/elasticsearch_api.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import requests
22
from flask_restful import Resource, abort
33
from flask import request, make_response
4-
from flask_cors import cross_origin
54
from pbench.server.api.resources.query_apis import get_es_url
65

76

@@ -12,7 +11,6 @@ def __init__(self, config, logger):
1211
self.logger = logger
1312
self.elasticsearch = get_es_url(config)
1413

15-
@cross_origin()
1614
def post(self):
1715
json_data = request.get_json(silent=True)
1816
if not json_data:

lib/pbench/server/api/resources/query_apis/query_controllers.py

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flask import request
1+
from flask import request, jsonify
22
from flask_restful import Resource, abort
33
import requests
44

@@ -94,14 +94,6 @@ def post(self):
9494
cluster by choosing unique prefixes to generate unique index
9595
names.
9696
97-
TODO: This implementation currently returns the raw Elasticsearch
98-
JSON response payload to the caller. The intent is to make this
99-
more attuned to the Pbench domain by removing the Elasticsearch
100-
"boilerplate" without dropping information required by a client
101-
(including, in particular, the Pbench dashboard). Essentially,
102-
this will extend the implementation up through the dashboard
103-
"model" layer instead of simply replacing the "service" layer.
104-
10597
Required headers include
10698
10799
X-auth-token: Pbench session authorization token authorized to access
@@ -115,6 +107,10 @@ def post(self):
115107
name, the number of runs using that controller name, and the start
116108
timestamp of the latest run both in binary and string form.
117109
110+
TODO: We probably need to return all *unowned* controllers when called
111+
without a session token or user parameter. We need to define precisely
112+
how this should work.
113+
118114
NOTE: This is the format currently constructed by the Pbench
119115
dashboard `src/model/dashboard.js` fetchControllers method, which
120116
becomes part of the Redux state.
@@ -131,23 +127,17 @@ def post(self):
131127
"""
132128
json_data = request.get_json(silent=True)
133129
if not json_data:
134-
self.logger.info(
135-
"QueryControllers: Invalid JSON object. Query: {}", request.url
136-
)
137-
abort(400, message="QueryControllers: Missing request payload")
130+
self.logger.info("Invalid JSON object. Query: {}", request.url)
131+
abort(400, message="Missing request payload")
138132

139133
try:
140134
user = json_data["user"]
141135
start_arg = json_data["start"]
142136
end_arg = json_data["end"]
143137
except KeyError:
144138
keys = [k for k in ("user", "start", "end") if k not in json_data]
145-
self.logger.info(
146-
"QueryControllers: Missing required JSON keys {}", ",".join(keys)
147-
)
148-
abort(
149-
400, message=f"QueryControllers: Missing request data: {','.join(keys)}"
150-
)
139+
self.logger.info("Missing required JSON keys {}", ",".join(keys))
140+
abort(400, message=f"Missing request data: {','.join(keys)}")
151141

152142
# We need to support a query for public data without requiring
153143
# authorization; however if an Authorization header is given,
@@ -167,7 +157,7 @@ def post(self):
167157
'"Authorization" header specifies unsupported or missing '
168158
' schema; use "Authorization: Bearer <JWT-token>"'
169159
)
170-
abort(401, message="QueryControllers: invalid user authorization")
160+
abort(401, message="invalid user authorization")
171161

172162
try:
173163
start = parser.parse(start_arg)
@@ -176,7 +166,7 @@ def post(self):
176166
self.logger.info(
177167
"Invalid start or end time string: {}, {}: {}", start_arg, end_arg, e
178168
)
179-
abort(400, message="QueryControllers: Invalid start or end time string")
169+
abort(400, message="Invalid start or end time string")
180170

181171
# We have nothing to authorize against yet, but print the specified
182172
# user and session token to prove we got them (and so the variables
@@ -234,7 +224,7 @@ def post(self):
234224
es_response.raise_for_status()
235225
except requests.exceptions.HTTPError as e:
236226
self.logger.exception("HTTP error {} from Elasticsearch post request", e)
237-
abort(500, message=f"HTTP error {e} from Elasticsearch")
227+
abort(502, message="INTERNAL ERROR")
238228
except requests.exceptions.ConnectionError:
239229
self.logger.exception(
240230
"Connection refused during the Elasticsearch post request"
@@ -244,22 +234,17 @@ def post(self):
244234
self.logger.exception(
245235
"Connection timed out during the Elasticsearch post request"
246236
)
247-
abort(
248-
504, message="Connection timed out, could not post to Elasticsearch",
249-
)
237+
abort(504, message="Connection timed out, could not post to Elasticsearch")
250238
except requests.exceptions.InvalidURL:
251239
self.logger.exception(
252240
"Invalid url {} during the Elasticsearch post request", uri
253241
)
254-
abort(
255-
500,
256-
message=f"Invalid Elasticsearch url {uri}, could not complete the post request",
257-
)
242+
abort(500, message="INTERNAL ERROR")
258243
except Exception as e:
259244
self.logger.exception(
260245
"Exception {!r} occurred during the Elasticsearch post request", e
261246
)
262-
abort(500, message=f"Could not post to Elasticsearch: {e!r}")
247+
abort(500, message="INTERNAL ERROR")
263248
else:
264249
controllers = []
265250
try:
@@ -276,7 +261,7 @@ def post(self):
276261
controllers.append(c)
277262
except KeyError:
278263
self.logger.exception("ES response not formatted as expected")
279-
abort(500, message="Elasticsearch response is odd")
264+
abort(500, message="INTERNAL ERROR")
280265
else:
281266
# construct response object
282-
return controllers
267+
return jsonify(controllers)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from flask import jsonify
2+
from flask_restful import Resource, abort
3+
import requests
4+
5+
from pbench.server.api.resources.query_apis import get_es_url, get_index_prefix
6+
7+
8+
class QueryMonthIndices(Resource):
9+
"""
10+
Abstracted Pbench API to get date-bounded controller data.
11+
"""
12+
13+
def __init__(self, config, logger):
14+
self.logger = logger
15+
self.es_url = get_es_url(config)
16+
self.prefix = get_index_prefix(config)
17+
18+
def get(self):
19+
"""
20+
GET to detect the month suffixes existing for run data indices.
21+
22+
Required headers include
23+
24+
Content-Type: application/json
25+
Accept: application/json
26+
27+
The return payload is a list of "YYYY-mm" date strings corresponding
28+
to the months in which tarballs were indexed into the appropriate
29+
`run-data` index. (E.g., `drb.v6.run-data.2020-11`). Note that this
30+
list is in DESCENDING order, so the earliest date is last.
31+
32+
NOTE: No authorization or input payload is required for this API.
33+
34+
NOTE: This is the format currently constructed by the Pbench
35+
dashboard `src/model/datastore.js` fetchMonthIndices method, which
36+
becomes part of the Redux state.
37+
38+
[
39+
"2020-12",
40+
"2020-11",
41+
"2020-04"
42+
]
43+
"""
44+
self.logger.info(
45+
"QueryMonthIndices GET for prefix {}", self.prefix,
46+
)
47+
48+
uri = f"{self.es_url}/_aliases"
49+
try:
50+
# query Elasticsearch
51+
es_response = requests.get(uri, headers={"Accept": "application/json"})
52+
es_response.raise_for_status()
53+
except requests.exceptions.HTTPError as e:
54+
self.logger.exception("HTTP error {} from Elasticsearch post request", e)
55+
abort(502, message="INTERNAL ERROR")
56+
except requests.exceptions.ConnectionError:
57+
self.logger.exception(
58+
"Connection refused during the Elasticsearch post request"
59+
)
60+
abort(502, message="Network problem, could not post to Elasticsearch")
61+
except requests.exceptions.Timeout:
62+
self.logger.exception(
63+
"Connection timed out during the Elasticsearch post request"
64+
)
65+
abort(504, message="Connection timed out, could not post to Elasticsearch")
66+
except requests.exceptions.InvalidURL:
67+
self.logger.exception(
68+
"Invalid url {} during the Elasticsearch post request", uri
69+
)
70+
abort(500, message="INTERNAL ERROR")
71+
except Exception as e:
72+
self.logger.exception(
73+
"Exception {!r} occurred during the Elasticsearch post request", e
74+
)
75+
abort(500, message="INTERNAL ERROR")
76+
else:
77+
months = []
78+
target = f"{self.prefix}.v6.run-data."
79+
try:
80+
es_json = es_response.json()
81+
self.logger.info("looking for {} in {}", target, es_json)
82+
for index in es_json.keys():
83+
if target in index:
84+
months.append(index.split(".")[-1])
85+
# The dashboard converts the strings to int (removing the '-')
86+
# and does a numeric sort; however since we always have a full
87+
# "YYYY-mm" date, this isn't necessary.
88+
months.sort(reverse=True)
89+
self.logger.info("found months {!r}", months)
90+
except KeyError:
91+
self.logger.exception("ES response not formatted as expected")
92+
abort(500, message="INTERNAL ERROR")
93+
else:
94+
# construct response object
95+
return jsonify(months)

lib/pbench/test/unit/server/test_query_controller.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ def test_missing_json_object(self, client, server_config):
6767
"""
6868
response = client.post(f"{server_config.rest_uri}/controllers/list")
6969
assert response.status_code == 400
70-
assert (
71-
response.json.get("message") == "QueryControllers: Missing request payload"
72-
)
70+
assert response.json.get("message") == "Missing request payload"
7371

7472
@pytest.mark.parametrize(
7573
"keys",
@@ -95,8 +93,7 @@ def test_missing_keys(self, client, server_config, keys):
9593
assert response.status_code == 400
9694
missing = [k for k in ("user", "start", "end") if k not in keys]
9795
assert (
98-
response.json.get("message")
99-
== f"QueryControllers: Missing request data: {','.join(missing)}"
96+
response.json.get("message") == f"Missing request data: {','.join(missing)}"
10097
)
10198

10299
def test_bad_dates(self, client, server_config):
@@ -113,10 +110,7 @@ def test_bad_dates(self, client, server_config):
113110
},
114111
)
115112
assert response.status_code == 400
116-
assert (
117-
response.json.get("message")
118-
== "QueryControllers: Invalid start or end time string"
119-
)
113+
assert response.json.get("message") == "Invalid start or end time string"
120114

121115
def test_query(self, client, server_config, query_helper):
122116
"""
@@ -182,7 +176,7 @@ def test_query(self, client, server_config, query_helper):
182176
@pytest.mark.parametrize(
183177
"exceptions",
184178
(
185-
{"exception": requests.exceptions.HTTPError, "status": 500},
179+
{"exception": requests.exceptions.HTTPError, "status": 502},
186180
{"exception": requests.exceptions.ConnectionError, "status": 502},
187181
{"exception": requests.exceptions.Timeout, "status": 504},
188182
{"exception": requests.exceptions.InvalidURL, "status": 500},

0 commit comments

Comments
 (0)