Skip to content

Commit

Permalink
Remove redirection usage APIs expected to be used through a load bala…
Browse files Browse the repository at this point in the history
…ncer (#5619)
  • Loading branch information
achamayou authored Sep 7, 2023
1 parent d37ecd6 commit 15e37de
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 83 deletions.
2 changes: 1 addition & 1 deletion .daily_canary
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-^- ___ ___
(- -) (= =) | Y & +--?
(- -) (= =) | Y & +--???
( V ) / . \ O +---=---'
/--x-m- /--n-n---xXx--/--yY------
18 changes: 16 additions & 2 deletions doc/schemas/node_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@
"info": {
"description": "This API provides public, uncredentialed access to service and node state.",
"title": "CCF Public Node API",
"version": "4.3.0"
"version": "4.6.0"
},
"openapi": "3.0.0",
"paths": {
Expand Down Expand Up @@ -1250,21 +1250,35 @@
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetNode__NodeInfo"
}
}
},
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/never"
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/node/network/nodes/self": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetNode__NodeInfo"
}
}
},
"description": "Default response description"
},
"default": {
Expand Down
3 changes: 3 additions & 0 deletions src/host/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,9 @@ int main(int argc, char** argv)
{
startup_config.network.acme = config.network.acme;
}
// Used by GET /node/network/nodes/self to return rpc interfaces
// prior to the KV being updated
startup_config.network.rpc_interfaces = config.network.rpc_interfaces;

LOG_INFO_FMT("Initialising enclave: enclave_create_node");
std::atomic<bool> ecall_completed = false;
Expand Down
135 changes: 76 additions & 59 deletions src/node/rpc/node_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ namespace ccf
openapi_info.description =
"This API provides public, uncredentialed access to service and node "
"state.";
openapi_info.document_version = "4.3.0";
openapi_info.document_version = "4.6.0";
}

void init_handlers() override
Expand Down Expand Up @@ -1117,94 +1117,111 @@ namespace ccf
.set_auto_schema<void, GetNode::Out>()
.install();

auto get_self_node = [this](auto& args) {
auto get_self_node = [this](auto& args, nlohmann::json&&) {
auto node_id = this->context.get_node_id();
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(node_id);
if (info)

bool is_primary = false;
if (consensus != nullptr)
{
auto& interface_id =
args.rpc_ctx->get_session_context()->interface_id;
if (!interface_id.has_value())
auto primary = consensus->primary();
if (primary.has_value() && primary.value() == node_id)
{
args.rpc_ctx->set_error(
is_primary = true;
}
}

if (info.has_value())
{
// Answers from the KV are preferred, as they are more up-to-date,
// especially status and node_data.
auto& ni = info.value();
return make_success(GetNode::Out{
node_id,
ni.status,
is_primary,
ni.rpc_interfaces,
ni.node_data,
nodes->get_version_of_previous_write(node_id).value_or(0)});
}
else
{
// If the node isn't in its KV yet, fall back to configuration
auto node_configuration_subsystem =
this->context.get_subsystem<NodeConfigurationSubsystem>();
if (!node_configuration_subsystem)
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
return;
"NodeConfigurationSubsystem is not available");
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format(
"https://{}/node/network/nodes/{}", address, node_id.value()));
return;
return make_success(GetNode::Out{
node_id,
ccf::NodeStatus::PENDING,
is_primary,
node_configuration_subsystem->get()
.node_config.network.rpc_interfaces,
node_configuration_subsystem->get().node_config.node_data,
0});
}

args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Node info not available");
return;
};
make_read_only_endpoint(
"/network/nodes/self", HTTP_GET, get_self_node, no_auth_required)
"/network/nodes/self",
HTTP_GET,
json_read_only_adapter(get_self_node),
no_auth_required)
.set_auto_schema<void, GetNode::Out>()
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.install();

auto get_primary_node = [this](auto& args) {
auto get_primary_node = [this](auto& args, nlohmann::json&&) {
if (consensus != nullptr)
{
auto node_id = this->context.get_node_id();
auto primary_id = consensus->primary();
if (!primary_id.has_value())
{
args.rpc_ctx->set_error(
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
return;
}

auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(node_id);
auto info_primary = nodes->get(primary_id.value());
if (info && info_primary)
auto info = nodes->get(primary_id.value());
if (!info)
{
auto& interface_id =
args.rpc_ctx->get_session_context()->interface_id;
if (!interface_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
return;
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format(
"https://{}/node/network/nodes/{}",
address,
primary_id->value()));
return;
return make_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"Node not found");
}
}

args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
return;
auto& ni = info.value();
return make_success(GetNode::Out{
primary_id.value(),
ni.status,
true,
ni.rpc_interfaces,
ni.node_data,
nodes->get_version_of_previous_write(primary_id.value())
.value_or(0)});
}
else
{
return make_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"No configured consensus");
}
};
make_read_only_endpoint(
"/network/nodes/primary", HTTP_GET, get_primary_node, no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
"/network/nodes/primary",
HTTP_GET,
json_read_only_adapter(get_primary_node),
no_auth_required)
.set_auto_schema<void, GetNode::Out>()
.install();

auto is_primary = [this](auto& args) {
Expand Down
56 changes: 35 additions & 21 deletions tests/e2e_common_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,7 @@ def test_network_node_info(network, args):
for interface_name in node.host.rpc_interfaces.keys():
primary_interface = primary.host.rpc_interfaces[interface_name]
with node.client(interface_name=interface_name) as c:
# /node/network/nodes/self is always a redirect
r = c.get("/node/network/nodes/self", allow_redirects=False)
assert r.status_code == http.HTTPStatus.PERMANENT_REDIRECT.value
node_interface = node.host.rpc_interfaces[interface_name]
assert (
r.headers["location"]
== f"https://{node_interface.public_host}:{node_interface.public_port}/node/network/nodes/{node.node_id}"
), r.headers["location"]

# Following that redirect gets you the node info
r = c.get("/node/network/nodes/self", allow_redirects=True)
assert r.status_code == http.HTTPStatus.OK.value
body = r.body.json()
assert body["node_id"] == node.node_id
Expand All @@ -83,10 +73,9 @@ def test_network_node_info(network, args):

for node in all_nodes:
for interface_name in node.host.rpc_interfaces.keys():
node_interface = node.host.rpc_interfaces[interface_name]
primary_interface = primary.host.rpc_interfaces[interface_name]
with node.client(interface_name=interface_name) as c:
# /node/primary is a 200 on the primary, and a redirect (to a 200) elsewhere
# HEAD /node/primary is a 200 on the primary, and a redirect (to a 200) elsewhere
r = c.head("/node/primary", allow_redirects=False)
if node != primary:
assert r.status_code == http.HTTPStatus.PERMANENT_REDIRECT.value
Expand All @@ -95,18 +84,9 @@ def test_network_node_info(network, args):
== f"https://{primary_interface.public_host}:{primary_interface.public_port}/node/primary"
), r.headers["location"]
r = c.head("/node/primary", allow_redirects=True)

assert r.status_code == http.HTTPStatus.OK.value

# /node/network/nodes/primary is always a redirect
r = c.get("/node/network/nodes/primary", allow_redirects=False)
assert r.status_code == http.HTTPStatus.PERMANENT_REDIRECT.value
actual = r.headers["location"]
expected = f"https://{node_interface.public_host}:{node_interface.public_port}/node/network/nodes/{primary.node_id}"
assert actual == expected, f"{actual} != {expected}"

# Following that redirect gets you the primary's node info
r = c.get("/node/network/nodes/primary", allow_redirects=True)
assert r.status_code == http.HTTPStatus.OK.value
body = r.body.json()
assert body == node_infos[primary.node_id]
Expand All @@ -121,6 +101,40 @@ def test_network_node_info(network, args):
body = r.body.json()
assert body == node_infos[target_node.node_id]

# Create a PENDING node and check that /node/network/nodes/self
# returns the correct information from configuration
operator_rpc_interface = "operator_rpc_interface"
host = infra.net.expand_localhost()
new_node = network.create_node(
infra.interfaces.HostSpec(
rpc_interfaces={
infra.interfaces.PRIMARY_RPC_INTERFACE: infra.interfaces.RPCInterface(
host=host, app_protocol="HTTP2" if args.http2 else "HTTP1"
),
operator_rpc_interface: infra.interfaces.RPCInterface(
host=host,
app_protocol="HTTP2" if args.http2 else "HTTP1",
endorsement=infra.interfaces.Endorsement(
authority=infra.interfaces.EndorsementAuthority.Node
),
),
}
)
)
network.join_node(new_node, args.package, args)

with new_node.client(interface_name=operator_rpc_interface) as c:
r = c.get("/node/network/nodes/self", allow_redirects=False)
assert r.status_code == http.HTTPStatus.OK.value
body = r.body.json()
assert body["node_id"] == new_node.node_id
assert (
infra.interfaces.HostSpec.to_json(new_node.host) == body["rpc_interfaces"]
)
assert body["status"] == NodeStatus.PENDING.value
assert body["primary"] is False
new_node.stop()

return network


Expand Down

0 comments on commit 15e37de

Please sign in to comment.