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

feat: add /internal-backstage/ready endpoint #252

Merged
merged 2 commits into from
Sep 12, 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
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ chrono = {version = "0.4.30", features = ["serde"]}
cidr = "0.2.2"
clap = {version = "4.4.2", features = ["derive", "env"]}
clap-markdown = "0.1.3"
dashmap = "5.5.0"
dashmap = "5.5.3"
dotenv = {version = "0.15.0", features = ["clap"]}
futures = "0.3.28"
futures-core = "0.3.28"
Expand All @@ -40,18 +40,18 @@ opentelemetry-prometheus = "0.12.0"
opentelemetry-semantic-conventions = "0.11.0"
prometheus = {version = "0.13.3", features = ["process"]}
prometheus-static-metric = "0.5.1"
redis = {version = "0.23.0", features = ["tokio-comp", "tokio-rustls-comp"]}
reqwest = {version = "0.11.18", default-features = false, features = ["rustls", "json", "rustls-tls", "native-tls"]}
redis = {version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"]}
reqwest = {version = "0.11.20", default-features = false, features = ["rustls", "json", "rustls-tls", "native-tls"]}
rustls = "0.21.6"
rustls-pemfile = "1.0.3"
serde = {version = "1.0.171", features = ["derive"]}
serde_json = "1.0.102"
serde = {version = "1.0.188", features = ["derive"]}
serde_json = "1.0.106"
serde_qs = { version = "0.12.0", features = ["actix4", "tracing"] }
shadow-rs = "0.23.0"
tokio = {version = "1.29.1", features = ["macros", "rt-multi-thread", "tracing", "fs"]}
tracing = {version = "0.1.37", features = ["log"]}
tracing-subscriber = {version = "0.3.17", features = ["json", "env-filter"]}
ulid = "1.0.0"
ulid = "1.1.0"
unleash-types = { version = "0.10", features = ["openapi", "hashes"]}
unleash-yggdrasil = { version = "0.5.9" }
utoipa = {version = "3", features = ["actix_extras", "chrono"]}
Expand Down
212 changes: 208 additions & 4 deletions server/src/internal_backstage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ use actix_web::{
get,
web::{self, Json},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use unleash_types::client_features::ClientFeatures;
#[derive(Debug, Serialize, Deserialize)]
pub struct EdgeStatus {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be time for the inner type to be an enum here. Should be okay to do something like

enum Status {
Ok,
Ready,
NotReady
}

Small suggestion, I'm okay for this to go without it

status: String,
}
Expand All @@ -18,7 +20,20 @@ impl EdgeStatus {
status: "OK".into(),
}
}

pub fn not_ready() -> Self {
EdgeStatus {
status: "NOT_READY".into(),
}
}

pub fn ready() -> Self {
EdgeStatus {
status: "READY".into(),
}
}
}

#[get("/health")]
pub async fn health() -> EdgeJsonResult<EdgeStatus> {
Ok(Json(EdgeStatus::ok()))
Expand All @@ -30,6 +45,17 @@ pub async fn info() -> EdgeJsonResult<BuildInfo> {
Ok(Json(data))
}

#[get("/ready")]
pub async fn ready(
features_cache: web::Data<DashMap<String, ClientFeatures>>,
) -> EdgeJsonResult<EdgeStatus> {
if features_cache.is_empty() {
Ok(Json(EdgeStatus::not_ready()))
} else {
Ok(Json(EdgeStatus::ready()))
}
}

#[get("/tokens")]
pub async fn tokens(
feature_refresher: web::Data<FeatureRefresher>,
Expand Down Expand Up @@ -62,17 +88,31 @@ pub fn configure_internal_backstage(
cfg.service(health)
.service(info)
.service(tokens)
.service(ready)
.service(web::resource("/metrics").route(web::get().to(metrics_handler)));
}

#[cfg(test)]
mod tests {
use crate::types::BuildInfo;
use std::str::FromStr;
use std::sync::Arc;

use crate::auth::token_validator::TokenValidator;
use crate::http::feature_refresher::FeatureRefresher;
use crate::http::unleash_client::UnleashClient;
use crate::internal_backstage::EdgeStatus;
use crate::middleware;
use crate::tests::upstream_server;
use crate::tokens::cache_key;
use crate::types::{BuildInfo, EdgeToken, TokenInfo, TokenType, TokenValidationStatus};
use actix_web::body::MessageBody;
use actix_web::http::header::ContentType;
use actix_web::test;
use actix_web::{web, App};

use chrono::Duration;
use dashmap::DashMap;
use unleash_types::client_features::{ClientFeature, ClientFeatures};
use unleash_yggdrasil::EngineState;
#[actix_web::test]
async fn test_health_ok() {
let app = test::init_service(
Expand Down Expand Up @@ -103,4 +143,168 @@ mod tests {
let info: BuildInfo = serde_json::from_slice(&body).unwrap();
assert_eq!(info.app_name, "unleash-edge");
}

#[actix_web::test]
async fn test_ready_endpoint_without_toggles() {
let client_features: DashMap<String, ClientFeatures> = DashMap::default();
let client_features_arc = Arc::new(client_features);
let app = test::init_service(
App::new()
.app_data(web::Data::from(client_features_arc))
.service(web::scope("/internal-backstage").service(super::ready)),
)
.await;
let req = test::TestRequest::get()
.uri("/internal-backstage/ready")
.insert_header(ContentType::json())
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let status: EdgeStatus = test::read_body_json(resp).await;
assert_eq!(status.status, "NOT_READY");
}

#[actix_web::test]
async fn test_ready_endpoint_with_toggles() {
let features = ClientFeatures {
features: vec![ClientFeature {
name: "test".to_string(),
..ClientFeature::default()
}],
query: None,
segments: None,
version: 2,
};
let client_features: DashMap<String, ClientFeatures> = DashMap::default();
client_features.insert(
"testproject:testenvironment.testtoken".into(),
features.clone(),
);
let client_features_arc = Arc::new(client_features);
let app = test::init_service(
App::new()
.app_data(web::Data::from(client_features_arc))
.service(web::scope("/internal-backstage").service(super::ready)),
)
.await;
let req = test::TestRequest::get()
.uri("/internal-backstage/ready")
.insert_header(ContentType::json())
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let status: EdgeStatus = test::read_body_json(resp).await;
assert_eq!(status.status, "READY");
}

#[actix_web::test]
async fn if_no_tokens_has_been_received_returns_empty_lists() {
let upstream_server = upstream_server(
Arc::new(DashMap::default()),
Arc::new(DashMap::default()),
Arc::new(DashMap::default()),
)
.await;
let unleash_client =
UnleashClient::new_insecure(upstream_server.url("/").as_str()).unwrap();
let arc_unleash_client = Arc::new(unleash_client);
let feature_refresher = FeatureRefresher {
tokens_to_refresh: Arc::new(DashMap::default()),
unleash_client: arc_unleash_client.clone(),
..Default::default()
};
let token_validator = TokenValidator {
unleash_client: arc_unleash_client.clone(),
token_cache: Arc::new(DashMap::default()),
persistence: None,
};
let app = test::init_service(
App::new()
.app_data(web::Data::new(feature_refresher))
.app_data(web::Data::new(token_validator))
.service(web::scope("/internal-backstage").service(super::tokens)),
)
.await;
let req = test::TestRequest::get()
.uri("/internal-backstage/tokens")
.insert_header(ContentType::json())
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let status: TokenInfo = test::read_body_json(resp).await;
assert!(status.token_refreshes.is_empty());
assert!(status.token_validation_status.is_empty());
}

#[actix_web::test]
async fn returns_validated_tokens() {
let upstream_features_cache: Arc<DashMap<String, ClientFeatures>> =
Arc::new(DashMap::default());
let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
let server = upstream_server(
upstream_token_cache.clone(),
upstream_features_cache.clone(),
upstream_engine_cache.clone(),
)
.await;
let upstream_features = crate::tests::features_from_disk("../examples/hostedexample.json");
let mut upstream_known_token = EdgeToken::from_str("dx:development.secret123").unwrap();
upstream_known_token.status = TokenValidationStatus::Validated;
upstream_known_token.token_type = Some(TokenType::Client);
upstream_token_cache.insert(
upstream_known_token.token.clone(),
upstream_known_token.clone(),
);
upstream_features_cache.insert(cache_key(&upstream_known_token), upstream_features.clone());
let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
let features_cache: Arc<DashMap<String, ClientFeatures>> = Arc::new(DashMap::default());
let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
let feature_refresher = Arc::new(FeatureRefresher {
unleash_client: unleash_client.clone(),
tokens_to_refresh: Arc::new(Default::default()),
features_cache: features_cache.clone(),
engine_cache: engine_cache.clone(),
refresh_interval: Duration::seconds(6000),
persistence: None,
});
let token_validator = Arc::new(TokenValidator {
unleash_client: unleash_client.clone(),
token_cache: token_cache.clone(),
persistence: None,
});
let local_app = test::init_service(
App::new()
.app_data(web::Data::from(token_validator.clone()))
.app_data(web::Data::from(features_cache.clone()))
.app_data(web::Data::from(engine_cache.clone()))
.app_data(web::Data::from(token_cache.clone()))
.app_data(web::Data::from(feature_refresher.clone()))
.service(web::scope("/internal-backstage").service(super::tokens))
.service(
web::scope("/api")
.wrap(middleware::as_async_middleware::as_async_middleware(
middleware::validate_token::validate_token,
))
.configure(crate::client_api::configure_client_api),
),
)
.await;
let client_request = test::TestRequest::get()
.uri("/api/client/features")
.insert_header(ContentType::json())
.insert_header(("Authorization", upstream_known_token.token.clone()))
.to_request();
let res = test::call_service(&local_app, client_request).await;
assert_eq!(res.status(), actix_http::StatusCode::OK);
let tokens_request = test::TestRequest::get()
.uri("/internal-backstage/tokens")
.insert_header(ContentType::json())
.to_request();
let token_res = test::call_service(&local_app, tokens_request).await;
let status: TokenInfo = test::read_body_json(token_res).await;
assert_eq!(status.token_refreshes.len(), 1);
assert_eq!(status.token_validation_status.len(), 1);
}
}