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

First RESTful HTTP API #399

Merged
merged 32 commits into from
Jul 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ec62a7c
Added generated code for REST API.
spble Jun 17, 2019
6de834d
Removed openapi generated code, because it was the rust client, not t…
spble Jun 18, 2019
3f82bfd
Added the correct rust-server code, automatically generated from open…
spble Jun 18, 2019
66f4ae3
Added generated code for REST API.
spble Jun 17, 2019
896990c
Removed openapi generated code, because it was the rust client, not t…
spble Jun 18, 2019
fa27fd1
Added the correct rust-server code, automatically generated from open…
spble Jun 18, 2019
9bca024
Included REST API in configuratuion.
spble Jun 24, 2019
955f483
Futher work on REST API.
spble Jun 24, 2019
cae4855
Added generated code for REST API.
spble Jun 17, 2019
bb524b0
Removed openapi generated code, because it was the rust client, not t…
spble Jun 18, 2019
a1fb5b5
Added the correct rust-server code, automatically generated from open…
spble Jun 18, 2019
839a150
Included REST API in configuratuion.
spble Jun 24, 2019
c36085d
Futher work on REST API.
spble Jun 24, 2019
925c7e4
Fixed merge conflict, bringing in latest master.
spble Jul 14, 2019
0baf97a
WIP: Restructured REST API to use hyper_router and separate services.
spble Jul 14, 2019
ab661e1
Fixing merge conflict.
spble Jul 14, 2019
14dffc1
WIP: Fixing rust for REST API
spble Jul 15, 2019
61d3a6e
WIP: Fixed up many bugs in trying to get router to compile.
spble Jul 15, 2019
8555f4c
WIP: Got the beacon_node to compile with the REST changes
spble Jul 16, 2019
dbe1634
Basic API works!
spble Jul 17, 2019
a9db924
WIP: Moved things around so that we can get state inside the handlers.
spble Jul 18, 2019
aec448b
WIP: Significant API updates.
spble Jul 22, 2019
302cc2a
WIP: Factored macros, defined API result and error.
spble Jul 22, 2019
8630a70
Fixed macros so that things compile.
spble Jul 23, 2019
5d1a8e4
Merge branch 'master' into rest-val-api
spble Jul 26, 2019
ffcae9f
Cleaned up code.
spble Jul 26, 2019
df14505
Removed auto-generated OpenAPI code.
spble Jul 26, 2019
ab3d36f
Addressed Paul's suggestions.
spble Jul 26, 2019
14f0538
Removed redundant validate_request function.
spble Jul 26, 2019
a34f101
Included graceful shutdown in Hyper server.
spble Jul 29, 2019
9b603f8
Fixing the dropped exit_signal, which prevented the API from starting.
spble Jul 29, 2019
fa2ecf8
Wrapped the exit signal, to get an API shutdown log line.
spble Jul 31, 2019
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"beacon_node/store",
"beacon_node/client",
"beacon_node/http_server",
"beacon_node/rest_api",
"beacon_node/network",
"beacon_node/eth2-libp2p",
"beacon_node/rpc",
Expand Down
1 change: 1 addition & 0 deletions beacon_node/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ store = { path = "../store" }
http_server = { path = "../http_server" }
eth2-libp2p = { path = "../eth2-libp2p" }
rpc = { path = "../rpc" }
rest_api = { path = "../rest_api" }
prometheus = "^0.6"
types = { path = "../../eth2/types" }
tree_hash = { path = "../../eth2/utils/tree_hash" }
Expand Down
3 changes: 3 additions & 0 deletions beacon_node/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct Config {
pub network: network::NetworkConfig,
pub rpc: rpc::RPCConfig,
pub http: HttpServerConfig,
pub rest_api: rest_api::APIConfig,
}

impl Default for Config {
Expand All @@ -31,6 +32,7 @@ impl Default for Config {
network: NetworkConfig::new(),
rpc: rpc::RPCConfig::default(),
http: HttpServerConfig::default(),
rest_api: rest_api::APIConfig::default(),
}
}
}
Expand Down Expand Up @@ -101,6 +103,7 @@ impl Config {
self.network.apply_cli_args(args)?;
self.rpc.apply_cli_args(args)?;
self.http.apply_cli_args(args)?;
self.rest_api.apply_cli_args(args)?;

if let Some(log_file) = args.value_of("logfile") {
self.log_file = PathBuf::from(log_file);
Expand Down
22 changes: 22 additions & 0 deletions beacon_node/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ extern crate slog;

mod beacon_chain_types;
mod config;

pub mod error;
pub mod notifier;

Expand Down Expand Up @@ -39,6 +40,8 @@ pub struct Client<T: BeaconChainTypes> {
pub http_exit_signal: Option<Signal>,
/// Signal to terminate the slot timer.
pub slot_timer_exit_signal: Option<Signal>,
/// Signal to terminate the API
pub api_exit_signal: Option<Signal>,
/// The clients logger.
log: slog::Logger,
/// Marker to pin the beacon chain generics.
Expand Down Expand Up @@ -143,6 +146,24 @@ where
None
};

// Start the `rest_api` service
let api_exit_signal = if client_config.rest_api.enabled {
match rest_api::start_server(
&client_config.rest_api,
executor,
beacon_chain.clone(),
&log,
) {
Ok(s) => Some(s),
Err(e) => {
error!(log, "API service failed to start."; "error" => format!("{:?}",e));
None
}
}
} else {
None
};

let (slot_timer_exit_signal, exit) = exit_future::signal();
if let Ok(Some(duration_to_next_slot)) = beacon_chain.slot_clock.duration_to_next_slot() {
// set up the validator work interval - start at next slot and proceed every slot
Expand Down Expand Up @@ -175,6 +196,7 @@ where
http_exit_signal,
rpc_exit_signal,
slot_timer_exit_signal: Some(slot_timer_exit_signal),
api_exit_signal,
log,
network,
phantom: PhantomData,
Expand Down
22 changes: 22 additions & 0 deletions beacon_node/rest_api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "rest_api"
version = "0.1.0"
authors = ["Luke Anderson <luke@lukeanderson.com.au>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
beacon_chain = { path = "../beacon_chain" }
version = { path = "../version" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "^1.0"
slog = "^2.2.3"
slog-term = "^2.4.0"
slog-async = "^2.3.0"
clap = "2.32.0"
http = "^0.1.17"
hyper = "0.12.32"
hyper-router = "^0.5"
futures = "0.1"
exit-future = "0.1.3"
tokio = "0.1.17"
65 changes: 65 additions & 0 deletions beacon_node/rest_api/src/beacon_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use beacon_chain::{BeaconChain, BeaconChainTypes};
use serde::Serialize;
use slog::info;
use std::sync::Arc;
use version;

use super::{path_from_request, success_response, APIResult, APIService};

use hyper::{Body, Request, Response};
use hyper_router::{Route, RouterBuilder};

#[derive(Clone)]
pub struct BeaconNodeServiceInstance<T: BeaconChainTypes + 'static> {
pub marker: std::marker::PhantomData<T>,
}

/// A string which uniquely identifies the client implementation and its version; similar to [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3).
#[derive(Serialize)]
pub struct Version(String);
impl From<String> for Version {
fn from(x: String) -> Self {
Version(x)
}
}

/// The genesis_time configured for the beacon node, which is the unix time at which the Eth2.0 chain began.
#[derive(Serialize)]
pub struct GenesisTime(u64);
impl From<u64> for GenesisTime {
fn from(x: u64) -> Self {
GenesisTime(x)
}
}

impl<T: BeaconChainTypes + 'static> APIService for BeaconNodeServiceInstance<T> {
fn add_routes(&mut self, router_builder: RouterBuilder) -> Result<RouterBuilder, hyper::Error> {
let router_builder = router_builder
.add(Route::get("/version").using(result_to_response!(get_version)))
.add(Route::get("/genesis_time").using(result_to_response!(get_genesis_time::<T>)));
Ok(router_builder)
}
}

/// Read the version string from the current Lighthouse build.
fn get_version(_req: Request<Body>) -> APIResult {
let ver = Version::from(version::version());
let body = Body::from(
serde_json::to_string(&ver).expect("Version should always be serialializable as JSON."),
);
Ok(success_response(body))
}

/// Read the genesis time from the current beacon chain state.
fn get_genesis_time<T: BeaconChainTypes + 'static>(req: Request<Body>) -> APIResult {
let beacon_chain = req.extensions().get::<Arc<BeaconChain<T>>>().unwrap();
let gen_time = {
let state = beacon_chain.current_state();
state.genesis_time
};
let body = Body::from(
serde_json::to_string(&gen_time)
.expect("Genesis should time always have a valid JSON serialization."),
);
Ok(success_response(body))
}
46 changes: 46 additions & 0 deletions beacon_node/rest_api/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use clap::ArgMatches;
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;

/// HTTP REST API Configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Enable the REST API server.
pub enabled: bool,
/// The IPv4 address the REST API HTTP server will listen on.
pub listen_address: Ipv4Addr,
/// The port the REST API HTTP server will listen on.
pub port: u16,
}

impl Default for Config {
fn default() -> Self {
Config {
enabled: true, // rest_api enabled by default
listen_address: Ipv4Addr::new(127, 0, 0, 1),
port: 1248,
}
}
}

impl Config {
pub fn apply_cli_args(&mut self, args: &ArgMatches) -> Result<(), &'static str> {
if args.is_present("api") {
self.enabled = true;
}

if let Some(rpc_address) = args.value_of("api-address") {
self.listen_address = rpc_address
.parse::<Ipv4Addr>()
.map_err(|_| "api-address is not a valid IPv4 address.")?;
}

if let Some(rpc_port) = args.value_of("api-port") {
self.port = rpc_port
.parse::<u16>()
.map_err(|_| "api-port is not a valid u16.")?;
}

Ok(())
}
}
132 changes: 132 additions & 0 deletions beacon_node/rest_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
extern crate futures;
extern crate hyper;
#[macro_use]
mod macros;
mod beacon_node;
pub mod config;

use beacon_chain::{BeaconChain, BeaconChainTypes};
pub use config::Config as APIConfig;

use slog::{info, o, warn};
use std::sync::Arc;
use tokio::runtime::TaskExecutor;

use crate::beacon_node::BeaconNodeServiceInstance;
use hyper::rt::Future;
use hyper::service::{service_fn, Service};
use hyper::{Body, Request, Response, Server, StatusCode};
use hyper_router::{RouterBuilder, RouterService};

pub enum APIError {
MethodNotAllowed { desc: String },
ServerError { desc: String },
NotImplemented { desc: String },
}

pub type APIResult = Result<Response<Body>, APIError>;

impl Into<Response<Body>> for APIError {
fn into(self) -> Response<Body> {
let status_code: (StatusCode, String) = match self {
APIError::MethodNotAllowed { desc } => (StatusCode::METHOD_NOT_ALLOWED, desc),
APIError::ServerError { desc } => (StatusCode::INTERNAL_SERVER_ERROR, desc),
APIError::NotImplemented { desc } => (StatusCode::NOT_IMPLEMENTED, desc),
};
Response::builder()
.status(status_code.0)
.body(Body::from(status_code.1))
.expect("Response should always be created.")
}
}

pub trait APIService {
fn add_routes(&mut self, router_builder: RouterBuilder) -> Result<RouterBuilder, hyper::Error>;
}

pub fn start_server<T: BeaconChainTypes + Clone + 'static>(
config: &APIConfig,
executor: &TaskExecutor,
beacon_chain: Arc<BeaconChain<T>>,
log: &slog::Logger,
) -> Result<exit_future::Signal, hyper::Error> {
let log = log.new(o!("Service" => "API"));

// build a channel to kill the HTTP server
let (exit_signal, exit) = exit_future::signal();

let exit_log = log.clone();
let server_exit = exit.and_then(move |_| {
info!(exit_log, "API service shutdown");
Ok(())
});

// Get the address to bind to
let bind_addr = (config.listen_address, config.port).into();

// Clone our stateful objects, for use in service closure.
let server_log = log.clone();
let server_bc = beacon_chain.clone();

// Create the service closure
let service = move || {
//TODO: This router must be moved out of this closure, so it isn't rebuilt for every connection.
let mut router = build_router_service::<T>();

// Clone our stateful objects, for use in handler closure
let service_log = server_log.clone();
let service_bc = server_bc.clone();

// Create a simple handler for the router, inject our stateful objects into the request.
service_fn(move |mut req| {
req.extensions_mut()
.insert::<slog::Logger>(service_log.clone());
req.extensions_mut()
.insert::<Arc<BeaconChain<T>>>(service_bc.clone());
router.call(req)
})
};

let server = Server::bind(&bind_addr)
.serve(service)
.with_graceful_shutdown(server_exit)
.map_err(move |e| {
warn!(
log,
"API failed to start, Unable to bind"; "address" => format!("{:?}", e)
)
});

executor.spawn(server);

Ok(exit_signal)
}

fn build_router_service<T: BeaconChainTypes + 'static>() -> RouterService {
let mut router_builder = RouterBuilder::new();

let mut bn_service: BeaconNodeServiceInstance<T> = BeaconNodeServiceInstance {
marker: std::marker::PhantomData,
};

router_builder = bn_service
.add_routes(router_builder)
.expect("The routes should always be made.");

RouterService::new(router_builder.build())
}

fn path_from_request(req: &Request<Body>) -> String {
req.uri()
.path_and_query()
.as_ref()
.map(|pq| String::from(pq.as_str()))
.unwrap_or(String::new())
}

fn success_response(body: Body) -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.body(body)
.expect("We should always be able to make response from the success body.")
}
23 changes: 23 additions & 0 deletions beacon_node/rest_api/src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
macro_rules! result_to_response {
($handler: path) => {
|req: Request<Body>| -> Response<Body> {
let log = req
.extensions()
.get::<slog::Logger>()
.expect("Our logger should be on req.")
.clone();
let path = path_from_request(&req);
let result = $handler(req);
match result {
Ok(response) => {
info!(log, "Request successful: {:?}", path);
response
}
Err(e) => {
info!(log, "Request failure: {:?}", path);
e.into()
}
}
}
};
}
Loading