Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URI to TOC API #3471

Merged
merged 3 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dashboard/src/actions/tableOfContentActions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as TYPES from "./types";
import API from "../utils/axiosInstance";
import { uriTemplate } from "../utils/helper";
import { showToast } from "./toastActions";
import { DANGER } from "assets/constants/toastConstants";

export const fetchTOC =
(param, parent, callForSubData) => async (dispatch, getState) => {
Expand All @@ -18,7 +20,8 @@ export const fetchTOC =
});
}
} catch (error) {
return error;
const msg = error.response?.data?.message;
dispatch(showToast(DANGER, msg ?? `Error response: ${error}`));
}
};

Expand Down
10 changes: 5 additions & 5 deletions dashboard/src/modules/components/TableOfContent/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const TableOfContent = () => {
? initialBreadcrumb(breadCrumbLabels)
: appGroupingBreadcrumb(false, breadCrumbLabels)
);
const dirPath = param.concat(`${firstHierarchyLevel ? "" : "/"}`, data);
const dirPath = param.concat(firstHierarchyLevel ? "" : "/", data);
setParam(dirPath);
setIsLoading(true);
getSubFolderData(dirPath);
Expand Down Expand Up @@ -241,7 +241,7 @@ const TableOfContent = () => {
key={index}
direction="down"
onClick={() => {
attachBreadCrumbs(data, true);
attachBreadCrumbs(data.name, true);
}}
drilldownMenu={
<DrilldownMenu id="drilldownMenuStart">
Expand All @@ -255,11 +255,11 @@ const TableOfContent = () => {
key={index}
direction="down"
onClick={() => {
attachBreadCrumbs(data, false);
attachBreadCrumbs(data.name, false);
}}
>
<FolderIcon />
{data}
{data.name}
</MenuItem>
);
} else {
Expand Down Expand Up @@ -288,7 +288,7 @@ const TableOfContent = () => {
}
>
<FolderIcon />
{data}
{data.name}
</MenuItem>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/reducers/tableOfContentReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const TableOfContentReducer = (state = initialState, action = {}) => {
stack: [...state.stack, payload],
searchSpace: payload.files,
tableData: payload.files,
currData: payload,
contentData: payload,
isLoading: false,
};

Expand Down
64 changes: 64 additions & 0 deletions lib/pbench/server/api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,19 @@ def authorize(
return self.schemas[method].authorize(args)


class HostSource(Enum):
REQUEST = auto()
FORWARDED = auto()
X_FORWARDED = auto()


@dataclass
class UriBase:
host: str
host_source: HostSource
host_value: str


class ApiBase(Resource):
"""A base class for Pbench queries that provides common parameter handling
behavior for specialized subclasses.
Expand Down Expand Up @@ -1628,6 +1641,57 @@ def _set_dataset_metadata(
fail[k] = str(e)
return fail

HEADER_FORWARD = re.compile(r";\s*host\s*=\s*(?P<host>[^;\s]+)")
X_HEADER_FORWARD = re.compile(r"\s*(?P<host>[^;\s,]+)")
webbnh marked this conversation as resolved.
Show resolved Hide resolved
PARAM_TEMPLATE = re.compile(r"/<(?P<type>[^:]+):(?P<name>\w+)>")

def _get_uri_base(self, request: Request) -> UriBase:
"""Determine the original request URI

When a request is directed through reverse proxies, the origin we see
may not be the URI used by the client to arrive here. When we're
providing a follow-up URI, we want to decode the forwarding headers to
avoid dropping a proxy (which may provide important infrastructure
controls and visibility).

Args:
request: the original HTTP Request

Return:
An object describing the origin and how we got there
"""
current_app.logger.debug(
"Received headers: {!r}, access_route {!r}, base_url {!r}, host {!r}, host_url {!r}",
request.headers,
request.access_route,
request.base_url,
request.host,
request.host_url,
)
origin = None
host_source = HostSource.REQUEST
host_value = request.host
header = request.headers.get("Forwarded")
if header:
m = self.HEADER_FORWARD.search(header)
if m:
origin = m.group("host")
host_source = HostSource.FORWARDED
host_value = header
if not origin:
header = request.headers.get("X-Forwarded-Host")
if header:
m = self.X_HEADER_FORWARD.match(header)
if m:
origin = m.group("host")
host_source = HostSource.X_FORWARDED
host_value = header
if not origin:
origin = host_value
proto = request.headers.get("X-Forwarded-Proto", "https")
host = f"{proto}://{origin}"
return UriBase(host, host_source, host_value)

def _dispatch(
self,
method: ApiMethod,
Expand Down
81 changes: 25 additions & 56 deletions lib/pbench/server/api/resources/endpoint_configure.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
from http import HTTPStatus
import re
from typing import Any, Dict
from typing import Any
from urllib.parse import urljoin

from flask import current_app, jsonify, request
from flask_restful import abort, Resource
from flask import current_app, jsonify, Request, Response

from pbench.server import PbenchServerConfig
from pbench.server import OperationCode, PbenchServerConfig
from pbench.server.api.resources import (
ApiBase,
ApiContext,
APIInternalError,
ApiMethod,
ApiParams,
ApiSchema,
)


class EndpointConfig(Resource):
class EndpointConfig(ApiBase):
"""
This supports dynamic dashboard configuration from the Pbench server rather
than constructing a static dashboard config file.
"""

forward_pattern = re.compile(r";\s*host\s*=\s*(?P<host>[^;\s]+)")
x_forward_pattern = re.compile(r"\s*(?P<host>[^;\s,]+)")
param_template = re.compile(r"/<(?P<type>[^:]+):(?P<name>\w+)>")

def __init__(self, config: PbenchServerConfig):
"""
__init__ Construct the API resource
Expand All @@ -42,9 +43,10 @@ def __init__(self, config: PbenchServerConfig):
proxying was set up for the original endpoints query: e.g., the
Javascript `window.origin` from which the Pbench dashboard was loaded.
"""
super().__init__(config, ApiSchema(ApiMethod.GET, OperationCode.READ))
self.server_config = config

def get(self):
def _get(self, args: ApiParams, request: Request, context: ApiContext) -> Response:
"""
Return server configuration information required by web clients
including the Pbench dashboard UI. This includes:
Expand Down Expand Up @@ -76,43 +78,16 @@ def get(self):

template.replace('{target_username}', 'value')
"""
current_app.logger.debug(
"Received headers: {!r}, access_route {!r}, base_url {!r}, host {!r}, host_url {!r}",
request.headers,
request.access_route,
request.base_url,
request.host,
request.host_url,
)
origin = None
host_source = "request"
host_value = request.host
header = request.headers.get("Forwarded")
if header:
m = self.forward_pattern.search(header)
if m:
origin = m.group("host")
host_source = "Forwarded"
host_value = header
if not origin:
header = request.headers.get("X-Forwarded-Host")
if header:
m = self.x_forward_pattern.match(header)
if m:
origin = m.group("host")
host_source = "X-Forwarded-Host"
host_value = header
if not origin:
origin = host_value
proto = request.headers.get("X-Forwarded-Proto", "https")
host = f"{proto}://{origin}"
origin = self._get_uri_base(request)
current_app.logger.info(
"Advertising endpoints at {} relative to {} ({})",
host,
host_source,
host_value,
origin.host,
origin.host_source,
origin.host_value,
)

host = origin.host

templates = {}

# Iterate through the Flask endpoints to add a description for each.
Expand All @@ -122,9 +97,9 @@ def get(self):
# Ignore anything that doesn't use our API prefix, because it's
# not in our API.
if url.startswith(self.server_config.rest_uri):
simplified = self.param_template.sub(r"/{\g<name>}", url)
matches = self.param_template.finditer(url)
template: Dict[str, Any] = {
simplified = self.PARAM_TEMPLATE.sub(r"/{\g<name>}", url)
matches = self.PARAM_TEMPLATE.finditer(url)
template: dict[str, Any] = {
"template": urljoin(host, simplified),
"params": {
match.group("name"): {"type": match.group("type")}
Expand Down Expand Up @@ -160,12 +135,6 @@ def get(self):
}

try:
response = jsonify(endpoints)
return jsonify(endpoints)
except Exception:
current_app.logger.exception(
"Something went wrong constructing the endpoint info"
)
abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR")
else:
response.status_code = HTTPStatus.OK
return response
APIInternalError("Something went wrong constructing the endpoint info")
2 changes: 2 additions & 0 deletions lib/pbench/server/api/resources/query_apis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ def _post(
uri_params: URI encoded keyword-arg supplied by the Flask
framework
"""
context["request"] = request
return self._call(requests.post, params, context)

def _get(
Expand All @@ -484,6 +485,7 @@ def _get(
uri_params: URI encoded keyword-arg supplied by the Flask
framework
"""
context["request"] = request
return self._call(requests.get, params, context)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ def postprocess(self, es_json: JSON, context: ApiContext) -> JSON:
{
"directories":
[
"sample1"
{
"name": "sample1",
"uri": "https://host/api/v1/datasets/id/contents/1-default/sample1"
}
],
"files": [
{
Expand All @@ -183,24 +186,35 @@ def postprocess(self, es_json: JSON, context: ApiContext) -> JSON:
"size": 0,
"mode": "0o777",
"type": "sym",
"linkpath": "sample1"
"linkpath": "sample1",
"uri": "https://host/api/v1/datasets/id/inventory/1-default/reference-result"
}
]
}
"""
request = context["request"]
resource_id = context["dataset"].resource_id
target = context["target"]
if len(es_json["hits"]["hits"]) == 0:
raise PostprocessError(
HTTPStatus.NOT_FOUND,
f"No directory '{context['target']}' in '{context['dataset']}' contents.",
f"No directory {target!r} in {resource_id!r} contents.",
)

origin = f"{self._get_uri_base(request).host}/datasets/{resource_id}"

dir_list = []
file_list = []
for val in es_json["hits"]["hits"]:
if val["_source"]["directory"] == context["target"]:
if val["_source"]["directory"] == target:
# Retrieve files list if present else add an empty list.
file_list = val["_source"].get("files", [])
elif val["_source"]["parent"] == context["target"]:
dir_list.append(val["_source"]["name"])
for f in val["_source"].get("files", []):
f["uri"] = f"{origin}/inventory{target}/{f['name']}"
file_list.append(f)
elif val["_source"]["parent"] == target:
name = val["_source"]["name"]
dir_list.append(
{"name": name, "uri": f"{origin}/contents{target}/{name}"}
)

return {"directories": dir_list, "files": file_list}
19 changes: 17 additions & 2 deletions lib/pbench/test/unit/server/query_apis/test_datasets_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ def test_query(
if expected_status == HTTPStatus.OK:
res_json = response.json
expected_result = {
"directories": ["sample1"],
"directories": [
{
"name": "sample1",
"uri": "https://localhost/datasets/random_md5_string1/contents/1-default/sample1",
}
],
"files": [
{
"name": "reference-result",
Expand All @@ -168,6 +173,7 @@ def test_query(
"mode": "0o777",
"type": "sym",
"linkpath": "sample1",
"uri": "https://localhost/datasets/random_md5_string1/inventory/1-default/reference-result",
}
],
}
Expand Down Expand Up @@ -270,7 +276,15 @@ def test_subdirectory_query(
)
if expected_status == HTTPStatus.OK:
res_json = response.json
expected_result = {"directories": ["sample1"], "files": []}
expected_result = {
"directories": [
{
"name": "sample1",
"uri": "https://localhost/datasets/random_md5_string1/contents/1-default/sample1",
}
],
"files": [],
}
assert expected_result == res_json

def test_files_query(
Expand Down Expand Up @@ -353,6 +367,7 @@ def test_files_query(
"size": 122,
"mode": "0o644",
"type": "reg",
"uri": "https://localhost/datasets/random_md5_string1/inventory/1-default/default.csv",
}
],
}
Expand Down