Skip to content

Commit f365ec0

Browse files
committed
Change to dynamically construct API set from Flask
Note that the unit test remains as it was, as a check that we're generating the proper set; this will have to be maintained as our API expands, but that seems like a worthwhile validation.
1 parent 37e1d88 commit f365ec0

File tree

2 files changed

+77
-56
lines changed

2 files changed

+77
-56
lines changed

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

+72-51
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from flask.globals import current_app
23

34
from flask_restful import Resource, abort
45
from flask import request, jsonify
@@ -14,6 +15,10 @@ class EndpointConfig(Resource):
1415
config file.
1516
"""
1617

18+
forward_pattern = re.compile(r";\s*host\s*=\s*(?P<host>[^;\s]*)")
19+
x_forward_pattern = re.compile(r"^\s*(?P<host>[^;\s,]*)")
20+
param_template = re.compile(r"<[\w_\d]+:[\d_\w]+>")
21+
1722
def __init__(self, config, logger):
1823
"""
1924
__init__ Construct the API resource
@@ -22,21 +27,19 @@ def __init__(self, config, logger):
2227
config (PbenchServerConfig): server config values
2328
logger (Logger): message logging
2429
25-
Report the server configuration to a web client; web access can be
26-
redirected through an external reverse proxy like NGINX by setting
27-
the "proxy_host" configuration setting in the "pbench-server"
28-
section of the pbench-server.cfg file. By default, the Pbench
29-
server uses a local Apache reverse proxy routing through the HTTP
30-
port (80), but this can be changed to any host name and port. All
31-
server endpoints will be reported with respect to that address.
30+
Report the server configuration to a web client. By default, the Pbench
31+
server ansible script sets up a local Apache reverse proxy routing
32+
through the HTTP port (80); an external reverse-proxy can be configured
33+
without the knowledge of the server, and this API will use reverse-proxy
34+
Forwarded or X-Forwarded-Host HTTP headers to discover the proxy
35+
configuration. All server endpoints will be reported with respect to that
36+
address.
3237
"""
3338
self.logger = logger
3439
self.host = config.get("pbench-server", "host")
3540
self.uri_prefix = config.rest_uri
3641
self.prefix = get_index_prefix(config)
3742
self.commit_id = config.COMMIT_ID
38-
self.forward_pattern = re.compile(r";\s*host\s*=\s*(?P<host>[^;\s]*)")
39-
self.x_forward_pattern = re.compile(r"^\s*(?P<host>[^;\s,]*)")
4043

4144
def get(self):
4245
"""
@@ -56,25 +59,17 @@ def get(self):
5659
schema, this is "v6.run-data."
5760
run_toc_index: The Elasticsearch V7 index for run TOC data. In
5861
the current ES schema, this is "v6.run-toc."
59-
api: A list of the server APIs supported; we give a name, which
62+
api: A dict of the server APIs supported; we give a name, which
6063
identifies the service, and the full URI relative to the
61-
configured host name and port (local or remote reverse proxy)
62-
results: Direct access to Pbench data sets for the dashboard; this
63-
is currently direct HTTP access through Apache public_html, and
64-
not an "API" as such; we will need to adopt this as a real
65-
server API when Pbench moves from local filesystem to S3 storage
66-
schemas
67-
elasticsearch: The Pbench pass-through URI for the Elasticsearch
68-
cluster. This will eventually be superceded by the native
69-
Pbench APIs, though it might remain accessible with special
70-
user/group privileges to support special cases
71-
graphql: The GraphQL frontend on postgreSQL currently used by the
72-
dashboard user mocks. This will be superceded and deprecated
73-
by native Pbench user management APIs
74-
queryControllers: Return information about the run documents that
75-
were created within a specified range of months.
76-
queryMonthIndices: Return the YYYY-mm date strings for all ES
77-
vx.run-data.* indices, in descending order
64+
configured host name and port (local or remote reverse proxy).
65+
66+
This is dynamically generated by processing the Flask URI
67+
rules; refer to api/__init__.py for the code which creates
68+
those mappings, or test_endpoint_configure.py for code that
69+
validates the current set (and must be updated when the API
70+
set changes). We supplement the Flask API list with the
71+
"results" API, which is currently just an Apache public_html
72+
file mapping but is referenced by the dashboard code.
7873
7974
TODO: We need an internal mechanism to track the active versions of the
8075
various Elasticsearch template documents. We're hardcoding them here and
@@ -86,31 +81,70 @@ def get(self):
8681
implementations. The entire "indices" section can be removed once that is
8782
resolved.
8883
"""
89-
self.logger.info("Received these headers: {!r}", request.headers)
90-
header = request.headers.get("Forwarded")
84+
self.logger.debug("Received these headers: {!r}", request.headers)
9185
origin = None
86+
host_source = "configuration"
87+
host_value = self.host
88+
header = request.headers.get("Forwarded")
9289
if header:
9390
m = self.forward_pattern.search(header)
9491
if m:
9592
origin = m.group("host")
96-
self.logger.info("Forwarded: {}, ({})", header, origin)
93+
host_source = "Forwarded"
94+
host_value = header
9795
if not origin:
9896
header = request.headers.get("X-Forwarded-Host")
9997
if header:
10098
m = self.x_forward_pattern.search(header)
10199
if m:
102100
origin = m.group("host")
103-
self.logger.info("X-Forwarded-Host: {}, ({})", header, origin)
101+
host_source = "X-Forwarded-Host"
102+
host_value = header
104103
if not origin:
105104
origin = self.host
106105
host = f"http://{origin}"
107-
uri = urljoin(host, self.uri_prefix)
108-
self.logger.info("'{}' : '{}' : '{}' : '{}'", self.host, origin, host, uri)
106+
self.logger.info(
107+
"Advertising endpoints relative to {}: {}", host_source, host_value
108+
)
109109

110-
# Strip a trailing slash because the paths we'll add have an
111-
# initial slash and we don't want two.
112-
if uri.endswith("/"):
113-
uri = uri.split(0, -1)
110+
# We pre-load the APIs list with the "results" link, which isn't yet
111+
# a Pbench server API. The dashboard static endpoint configuration
112+
# included this, and it makes sense for consistency.
113+
apis = {"results": urljoin(host, "/results")}
114+
115+
# Iterate through the Flask endpoints to add a description for each.
116+
for rule in current_app.url_map.iter_rules():
117+
url = str(rule.rule)
118+
119+
# Ignore anything that doesn't use our API prefix, because it's
120+
# not in our API.
121+
if url.startswith(self.uri_prefix):
122+
# If the URI is parameterized with a Flask "<type:name>"
123+
# template string, we don't want to report it, so we remove
124+
# it from the URI. We derive an API name by converting the
125+
# "/" characters in the URI to "_", after removing a trailing
126+
# "/" that would have been left by removing a template... we
127+
# don't remove the trailing "/" from the URI, which serves as
128+
# an indication that the parameter is needed.
129+
#
130+
# E.g, "/api/v1/controllers/list" yields:
131+
# "controllers_list": "/api/v1/controllers/list"
132+
#
133+
# while "/api/v1/users/<string:username>" yields:
134+
# "users": "/api/v1/users/"
135+
#
136+
# TODO: This won't work right with embedded template strings,
137+
# which we're not currently using anywhere; but it'll require
138+
# adjustment later if we add any. (E.g., something like
139+
# "/api/v1/foo/<string:name>/detail/<string:param>")
140+
m = re.search(self.param_template, url)
141+
if m:
142+
url = re.sub(self.param_template, "", url)
143+
path = url[len(self.uri_prefix) + 1 :]
144+
if path.endswith("/"):
145+
path = path[:-1]
146+
path = path.replace("/", "_")
147+
apis[path] = urljoin(host, url)
114148

115149
try:
116150
endpoints = {
@@ -121,20 +155,7 @@ def get(self):
121155
"result_index": f"{self.prefix}.v5.result-data-sample.",
122156
"result_data_index": f"{self.prefix}.v5.result-data.",
123157
},
124-
"api": {
125-
"results": f"{host}/results",
126-
"elasticsearch": f"{uri}/elasticsearch",
127-
"endpoints": f"{uri}/endpoints",
128-
"graphql": f"{uri}/graphql",
129-
"controller_list": f"{uri}/controllers/list",
130-
"controller_months": f"{uri}/controllers/months",
131-
"dataset_list": f"{uri}/datasets/list",
132-
"dataset": f"{uri}/datasets",
133-
"register": f"{uri}/register",
134-
"login": f"{uri}/login",
135-
"logout": f"{uri}/logout",
136-
"user": f"{uri}/user/<string:username>",
137-
},
158+
"api": apis,
138159
}
139160
response = jsonify(endpoints)
140161
except Exception:

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ def check_config(self, client, server_config, host, my_headers={}):
5353
"elasticsearch": f"{uri}/elasticsearch",
5454
"endpoints": f"{uri}/endpoints",
5555
"graphql": f"{uri}/graphql",
56-
"controller_list": f"{uri}/controllers/list",
57-
"controller_months": f"{uri}/controllers/months",
58-
"dataset_list": f"{uri}/datasets/list",
59-
"dataset": f"{uri}/datasets",
56+
"controllers_list": f"{uri}/controllers/list",
57+
"controllers_months": f"{uri}/controllers/months",
6058
"register": f"{uri}/register",
6159
"login": f"{uri}/login",
6260
"logout": f"{uri}/logout",
63-
"user": f"{uri}/user/<string:username>",
61+
"user": f"{uri}/user/",
62+
"host_info": f"{uri}/host_info",
63+
"upload_ctrl": f"{uri}/upload/ctrl/",
6464
},
6565
}
6666

0 commit comments

Comments
 (0)