Skip to content

Commit

Permalink
feat: status page at GET / and GET /api/v1/status routes (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
MicaiahReid authored Sep 25, 2023
1 parent 483fb8b commit d4c2290
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
run: |
cargo install cargo-tarpaulin
cargo --version
cargo tarpaulin --out lcov
cargo tarpaulin --out lcov --features k8s_tests
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ tower-test = "0.4.0"
test-case = "3.1.0"
rand = "0.8.5"
serial_test = "2.0.0"

[features]
k8s_tests = []
8 changes: 5 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use stacks_devnet_api::api_config::ApiConfig;
use stacks_devnet_api::responder::Responder;
use stacks_devnet_api::routes::{
get_standardized_path_parts, handle_check_devnet, handle_delete_devnet, handle_get_devnet,
handle_new_devnet, handle_try_proxy_service, API_PATH,
handle_get_status, handle_new_devnet, handle_try_proxy_service, API_PATH,
};
use stacks_devnet_api::{Context, StacksDevnetApiK8sManager};
use std::env;
Expand Down Expand Up @@ -78,11 +78,13 @@ async fn handle_request(
)
});
let headers = request.headers().clone();
let responder = Responder::new(http_response_config, headers.clone()).unwrap();

let responder = Responder::new(http_response_config, headers.clone(), ctx.clone()).unwrap();
if method == &Method::OPTIONS {
return responder.ok();
}
if method == &Method::GET && (path == "/" || path == &format!("{API_PATH}status")) {
return handle_get_status(responder, ctx).await;
}
let auth_header = auth_config
.auth_header
.unwrap_or("x-auth-request-user".to_string());
Expand Down
65 changes: 60 additions & 5 deletions src/responder.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use hiro_system_kit::slog;
use hyper::{
header::{
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
Expand All @@ -8,26 +9,39 @@ use hyper::{
};
use std::convert::Infallible;

use crate::api_config::ResponderConfig;
use crate::{api_config::ResponderConfig, Context};

#[derive(Default)]
pub struct Responder {
allowed_origins: Vec<String>,
allowed_methods: Vec<String>,
allowed_headers: String,
headers: HeaderMap<HeaderValue>,
ctx: Context,
}

impl Default for Responder {
fn default() -> Self {
Responder {
allowed_origins: Vec::default(),
allowed_methods: Vec::default(),
allowed_headers: String::default(),
headers: HeaderMap::default(),
ctx: Context::empty(),
}
}
}
impl Responder {
pub fn new(
config: ResponderConfig,
headers: HeaderMap<HeaderValue>,
ctx: Context,
) -> Result<Responder, String> {
Ok(Responder {
allowed_origins: config.allowed_origins.unwrap_or_default(),
allowed_methods: config.allowed_methods.unwrap_or_default(),
allowed_headers: config.allowed_headers.unwrap_or("*".to_string()),
headers,
ctx,
})
}

Expand Down Expand Up @@ -60,20 +74,61 @@ impl Responder {

fn _respond(&self, code: StatusCode, body: String) -> Result<Response<Body>, Infallible> {
let builder = self.response_builder();
match builder.status(code).body(Body::try_from(body).unwrap()) {
let body = match Body::try_from(body) {
Ok(b) => b,
Err(e) => {
self.ctx.try_log(|logger| {
slog::error!(
logger,
"responder failed to create response body: {}",
e.to_string()
)
});
Body::empty()
}
};
match builder.status(code).body(body) {
Ok(r) => Ok(r),
Err(_) => unreachable!(),
Err(e) => {
self.ctx.try_log(|logger| {
slog::error!(
logger,
"responder failed to send response: {}",
e.to_string()
)
});
Ok(self
.response_builder()
.status(500)
.body(Body::empty())
.unwrap())
}
}
}

pub fn respond(&self, code: u16, body: String) -> Result<Response<Body>, Infallible> {
self._respond(StatusCode::from_u16(code).unwrap(), body)
self._respond(
StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
body,
)
}

pub fn ok(&self) -> Result<Response<Body>, Infallible> {
self._respond(StatusCode::OK, "Ok".into())
}

pub fn ok_with_json(&self, body: Body) -> Result<Response<Body>, Infallible> {
match self
.response_builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(body)
{
Ok(r) => Ok(r),
Err(e) => self.err_internal(format!("failed to send response: {}", e.to_string())),
}
}

pub fn err_method_not_allowed(&self, body: String) -> Result<Response<Body>, Infallible> {
self._respond(StatusCode::METHOD_NOT_ALLOWED, body)
}
Expand Down
31 changes: 24 additions & 7 deletions src/routes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use hiro_system_kit::slog;
use hyper::{Body, Client, Request, Response, StatusCode, Uri};
use hyper::{Body, Client, Request, Response, Uri};
use serde_json::json;
use std::{convert::Infallible, str::FromStr};

use crate::{
Expand All @@ -9,6 +10,27 @@ use crate::{
Context, StacksDevnetApiK8sManager,
};

const VERSION: &str = env!("CARGO_PKG_VERSION");
const PRJ_NAME: &str = env!("CARGO_PKG_NAME");

pub async fn handle_get_status(
responder: Responder,
ctx: Context,
) -> Result<Response<Body>, Infallible> {
let version_info = format!("{PRJ_NAME} v{VERSION}");
let version_info = json!({ "version": version_info });
let version_info = match serde_json::to_vec(&version_info) {
Ok(v) => v,
Err(e) => {
let msg = format!("failed to parse version info: {}", e.to_string());
ctx.try_log(|logger| slog::error!(logger, "{}", msg));
return responder.err_internal(msg);
}
};
let body = Body::from(version_info);
responder.ok_with_json(body)
}

pub async fn handle_new_devnet(
request: Request<Body>,
user_id: &str,
Expand Down Expand Up @@ -60,12 +82,7 @@ pub async fn handle_get_devnet(
) -> Result<Response<Body>, Infallible> {
match k8s_manager.get_devnet_info(&network).await {
Ok(devnet_info) => match serde_json::to_vec(&devnet_info) {
Ok(body) => Ok(responder
.response_builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(body))
.unwrap()),
Ok(body) => responder.ok_with_json(Body::from(body)),
Err(e) => {
let msg = format!(
"failed to form response body: NAMESPACE: {}, ERROR: {}",
Expand Down
27 changes: 21 additions & 6 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ use stacks_devnet_api::{
use test_case::test_case;
use tower_test::mock::{self, Handle};

const VERSION: &str = env!("CARGO_PKG_VERSION");
const PRJ_NAME: &str = env!("CARGO_PKG_NAME");
fn get_version_info() -> String {
format!("{{\"version\":\"{PRJ_NAME} v{VERSION}\"}}")
}
fn get_template_config() -> StacksDevnetConfig {
let file_path = "src/tests/fixtures/stacks-devnet-config.json";
let file = File::open(file_path)
Expand Down Expand Up @@ -115,6 +120,7 @@ enum TestBody {
#[test_case("/api/v1/network/{namespace}/stacks-node/v2/info/", Method::GET, None, true => using assert_failed_proxy; "proxies requests to downstream nodes")]
#[serial_test::serial]
#[tokio::test]
#[cfg_attr(not(feature = "k8s_tests"), ignore)]
async fn it_responds_to_valid_requests_with_deploy(
mut request_path: &str,
method: Method,
Expand Down Expand Up @@ -175,11 +181,14 @@ async fn it_responds_to_valid_requests_with_deploy(
}

#[test_case("any", Method::OPTIONS, false => is equal_to (StatusCode::OK, "Ok".to_string()); "200 for any OPTIONS request")]
#[test_case("/", Method::GET, false => is equal_to (StatusCode::OK, get_version_info()); "200 for GET /")]
#[test_case("/api/v1/status", Method::GET, false => is equal_to (StatusCode::OK, get_version_info()); "200 for GET /api/v1/status")]
#[test_case("/api/v1/network/{namespace}", Method::DELETE, true => using assert_cannot_delete_devnet_err; "409 for network DELETE request to non-existing network")]
#[test_case("/api/v1/network/{namespace}", Method::GET, true => using assert_not_all_assets_exist_err; "404 for network GET request to non-existing network")]
#[test_case("/api/v1/network/{namespace}", Method::HEAD, true => is equal_to (StatusCode::NOT_FOUND, "not found".to_string()); "404 for network HEAD request to non-existing network")]
#[test_case("/api/v1/network/{namespace}/stacks-node/v2/info/", Method::GET, true => using assert_not_all_assets_exist_err; "404 for proxy requests to downstream nodes of non-existing network")]
#[tokio::test]
#[cfg_attr(not(feature = "k8s_tests"), ignore)]
async fn it_responds_to_valid_requests(
mut request_path: &str,
method: Method,
Expand Down Expand Up @@ -328,15 +337,16 @@ async fn it_responds_to_invalid_request_header() {
assert_eq!(body_str, "missing required auth header".to_string());
}

#[test_case("/api/v1/network/test", Method::OPTIONS => is equal_to "Ok".to_string())]
#[test_case("/api/v1/status", Method::GET => is equal_to get_version_info() )]
#[test_case("/", Method::GET => is equal_to get_version_info())]
#[tokio::test]
async fn it_ignores_request_header_for_options_requests() {
async fn it_ignores_request_header_for_some_requests(request_path: &str, method: Method) -> String {
let (k8s_manager, ctx) = get_mock_k8s_manager().await;

let request_builder = Request::builder()
.uri("/api/v1/network/test")
.method(Method::OPTIONS);
let request_builder = Request::builder().uri(request_path).method(method);
let request: Request<Body> = request_builder.body(Body::empty()).unwrap();
let response = handle_request(
let mut response = handle_request(
request,
k8s_manager.clone(),
ApiConfig::default(),
Expand All @@ -345,6 +355,10 @@ async fn it_ignores_request_header_for_options_requests() {
.await
.unwrap();
assert_eq!(response.status(), 200);
let body = response.body_mut();
let bytes = body::to_bytes(body).await.unwrap().to_vec();
let body_str = String::from_utf8(bytes).unwrap();
body_str
}

#[test_case("" => is equal_to PathParts { route: String::new(), ..Default::default() }; "for empty path")]
Expand Down Expand Up @@ -399,7 +413,7 @@ fn responder_allows_configuring_allowed_origins() {
};
let mut headers = HeaderMap::new();
headers.append("ORIGIN", HeaderValue::from_str("example.com").unwrap());
let responder = Responder::new(config, headers).unwrap();
let responder = Responder::new(config, headers, Context::empty()).unwrap();
let builder = responder.response_builder();
let built_headers = builder.headers_ref().unwrap();
assert_eq!(built_headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), "*");
Expand All @@ -411,6 +425,7 @@ fn responder_allows_configuring_allowed_origins() {

#[serial_test::serial]
#[tokio::test]
#[cfg_attr(not(feature = "k8s_tests"), ignore)]
async fn namespace_prefix_config_prepends_header() {
let (k8s_manager, ctx) = get_k8s_manager().await;

Expand Down

0 comments on commit d4c2290

Please sign in to comment.