From bf01d144c361fa08587ea8d7a944ab76db7ea5b6 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 12 Sep 2023 11:12:56 +0200 Subject: [PATCH 1/2] feat: add /internal-backstage/ready endpoint --- Cargo.lock | 18 +++++++++--------- server/Cargo.toml | 12 ++++++------ server/src/internal_backstage.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index deda8b9f..d63d8a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.3", + "socket2 0.5.4", "tokio", "tracing", ] @@ -243,7 +243,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.3", + "socket2 0.5.4", "time", "url", ] @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" [[package]] name = "anstyle-parse" @@ -2329,9 +2329,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", @@ -2580,9 +2580,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys 0.48.0", @@ -2812,7 +2812,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "tracing", "windows-sys 0.48.0", diff --git a/server/Cargo.toml b/server/Cargo.toml index 887bed5a..685b1511 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -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" @@ -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"]} diff --git a/server/src/internal_backstage.rs b/server/src/internal_backstage.rs index e2fdd220..37b12e3d 100644 --- a/server/src/internal_backstage.rs +++ b/server/src/internal_backstage.rs @@ -6,7 +6,9 @@ use actix_web::{ get, web::{self, Json}, }; +use dashmap::DashMap; use serde::Serialize; +use unleash_types::client_features::ClientFeatures; #[derive(Debug, Serialize)] pub struct EdgeStatus { status: String, @@ -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 { Ok(Json(EdgeStatus::ok())) @@ -30,6 +45,17 @@ pub async fn info() -> EdgeJsonResult { Ok(Json(data)) } +#[get("/ready")] +pub async fn ready( + features_cache: web::Data>, +) -> EdgeJsonResult { + 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, @@ -62,6 +88,7 @@ pub fn configure_internal_backstage( cfg.service(health) .service(info) .service(tokens) + .service(ready) .service(web::resource("/metrics").route(web::get().to(metrics_handler))); } From 5772125b387e3967b318f5e8dfae8e3a176ad6b2 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 12 Sep 2023 14:00:02 +0200 Subject: [PATCH 2/2] chore: added tests --- server/src/internal_backstage.rs | 185 ++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 4 deletions(-) diff --git a/server/src/internal_backstage.rs b/server/src/internal_backstage.rs index 37b12e3d..eb297266 100644 --- a/server/src/internal_backstage.rs +++ b/server/src/internal_backstage.rs @@ -7,9 +7,9 @@ use actix_web::{ web::{self, Json}, }; use dashmap::DashMap; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use unleash_types::client_features::ClientFeatures; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct EdgeStatus { status: String, } @@ -94,12 +94,25 @@ pub fn configure_internal_backstage( #[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( @@ -130,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 = 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 = 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> = + Arc::new(DashMap::default()); + let upstream_token_cache: Arc> = Arc::new(DashMap::default()); + let upstream_engine_cache: Arc> = 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> = Arc::new(DashMap::default()); + let token_cache: Arc> = Arc::new(DashMap::default()); + let engine_cache: Arc> = 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); + } }