1
1
import re
2
+ from flask .globals import current_app
2
3
3
4
from flask_restful import Resource , abort
4
5
from flask import request , jsonify
@@ -14,6 +15,10 @@ class EndpointConfig(Resource):
14
15
config file.
15
16
"""
16
17
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
+
17
22
def __init__ (self , config , logger ):
18
23
"""
19
24
__init__ Construct the API resource
@@ -22,21 +27,19 @@ def __init__(self, config, logger):
22
27
config (PbenchServerConfig): server config values
23
28
logger (Logger): message logging
24
29
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.
32
37
"""
33
38
self .logger = logger
34
39
self .host = config .get ("pbench-server" , "host" )
35
40
self .uri_prefix = config .rest_uri
36
41
self .prefix = get_index_prefix (config )
37
42
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,]*)" )
40
43
41
44
def get (self ):
42
45
"""
@@ -56,25 +59,17 @@ def get(self):
56
59
schema, this is "v6.run-data."
57
60
run_toc_index: The Elasticsearch V7 index for run TOC data. In
58
61
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
60
63
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.
78
73
79
74
TODO: We need an internal mechanism to track the active versions of the
80
75
various Elasticsearch template documents. We're hardcoding them here and
@@ -86,31 +81,70 @@ def get(self):
86
81
implementations. The entire "indices" section can be removed once that is
87
82
resolved.
88
83
"""
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 )
91
85
origin = None
86
+ host_source = "configuration"
87
+ host_value = self .host
88
+ header = request .headers .get ("Forwarded" )
92
89
if header :
93
90
m = self .forward_pattern .search (header )
94
91
if m :
95
92
origin = m .group ("host" )
96
- self .logger .info ("Forwarded: {}, ({})" , header , origin )
93
+ host_source = "Forwarded"
94
+ host_value = header
97
95
if not origin :
98
96
header = request .headers .get ("X-Forwarded-Host" )
99
97
if header :
100
98
m = self .x_forward_pattern .search (header )
101
99
if m :
102
100
origin = m .group ("host" )
103
- self .logger .info ("X-Forwarded-Host: {}, ({})" , header , origin )
101
+ host_source = "X-Forwarded-Host"
102
+ host_value = header
104
103
if not origin :
105
104
origin = self .host
106
105
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
+ )
109
109
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 )
114
148
115
149
try :
116
150
endpoints = {
@@ -121,20 +155,7 @@ def get(self):
121
155
"result_index" : f"{ self .prefix } .v5.result-data-sample." ,
122
156
"result_data_index" : f"{ self .prefix } .v5.result-data." ,
123
157
},
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 ,
138
159
}
139
160
response = jsonify (endpoints )
140
161
except Exception :
0 commit comments