From 0b2537f4bd397c666d458589bf30f9322b0c9214 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 31 Jan 2023 10:48:24 +0200 Subject: [PATCH] Implement redis datasource (#25) * chore: refactors the way datasources are handled so that we can support more than a single data source, especially ones that may need to be mutable * chore: introduce test containers * feat: implement redis datasource --- Cargo.lock | 141 +++++++++++++++++- README.md | 6 +- development-guide.md | 9 +- server/Cargo.toml | 2 + server/src/cli.rs | 33 +++- server/src/client_api.rs | 2 +- server/src/data_sources/mod.rs | 2 + .../{ => data_sources}/offline_provider.rs | 21 +-- server/src/data_sources/redis_provider.rs | 58 +++++++ server/src/edge_api.rs | 3 +- server/src/error.rs | 15 +- server/src/frontend_api.rs | 28 ++-- server/src/internal_backstage.rs | 2 +- server/src/lib.rs | 11 ++ server/src/main.rs | 63 +++++--- server/src/metrics.rs | 2 - server/src/tls.rs | 3 +- server/src/types.rs | 16 +- server/tests/redis_test.rs | 32 ++++ 19 files changed, 381 insertions(+), 68 deletions(-) create mode 100644 server/src/data_sources/mod.rs rename server/src/{ => data_sources}/offline_provider.rs (72%) create mode 100644 server/src/data_sources/redis_provider.rs create mode 100644 server/src/lib.rs create mode 100644 server/tests/redis_test.rs diff --git a/Cargo.lock b/Cargo.lock index 41ce6999..30ed2f43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,6 +367,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard-stubs" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +dependencies = [ + "chrono", + "serde", + "serde_with", +] + [[package]] name = "brotli" version = "3.3.4" @@ -508,6 +519,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const_fn" version = "0.4.9" @@ -657,14 +678,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.2", + "darling_macro 0.14.2", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", ] [[package]] @@ -681,13 +726,24 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ - "darling_core", + "darling_core 0.14.2", "quote", "syn", ] @@ -720,7 +776,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ - "darling", + "darling 0.14.2", "proc-macro2", "quote", "syn", @@ -757,6 +813,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1014,6 +1071,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.8" @@ -1639,6 +1705,20 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8455fa3621f6b41c514946de66ea0531f57ca017b2e6c7cc368035ea5b46df" +dependencies = [ + "combine", + "itoa", + "percent-encoding", + "ryu", + "sha1_smol", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1810,6 +1890,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1821,6 +1923,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.6" @@ -1906,6 +2014,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.107" @@ -1948,6 +2062,23 @@ dependencies = [ "syn", ] +[[package]] +name = "testcontainers" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -2286,12 +2417,14 @@ dependencies = [ "opentelemetry", "opentelemetry-prometheus", "prometheus", + "redis", "rustls", "rustls-pemfile", "serde", "serde_json", "shadow-rs", "test-case", + "testcontainers", "tokio", "tracing", "tracing-opentelemetry", diff --git a/README.md b/README.md index a5c669bd..35dca9db 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ We support running in various modes, from a [local version](#offline) to a full You have a need to have full control of both the data your clients will get and which keys can be used to access the server. This mode needs a downloaded JSON dump of a result from a query against an Unleash server on the [/api/client/features](https://docs.getunleash.io/reference/api/unleash/get-client-feature) endpoint as well as a comma-separated list of keys that should be allowed to access the server. -If your keys follow the Unleash API key format `[project]:[environment].`, Edge will filter the features dump to match the project contained in the key. +If your keys follow the Unleash API key format `[project]:[environment].`, Edge will filter the features dump to match the project contained in the key. -If you'd rather use a simple key like `secret-123`, any query against `/api/client/features` will receive the dump passed in on the command line. +If you'd rather use a simple key like `secret-123`, any query against `/api/client/features` will receive the dump passed in on the command line. Any query against `/api/frontend` or `/api/proxy` with a valid key will receive only enabled toggles. To launch in this mode, run @@ -57,4 +57,4 @@ TODO: Document proxy mode TODO: Document edge mode ## Development -See our [Contributors guide](./CONTRIBUTING.md) as well as our [development-guide](./development-guide.md) +See our [Contributors guide](./CONTRIBUTING.md) as well as our [development-guide](./development-guide.md) \ No newline at end of file diff --git a/development-guide.md b/development-guide.md index eacc2001..99110cd1 100644 --- a/development-guide.md +++ b/development-guide.md @@ -1,6 +1,6 @@ ### Tools * Install Rust using [rustup](https://rustup.rs) -* Copy the pre-commit hook in the hooks folder into .git/hooks/pre-commit +* Copy the pre-commit hook in the hooks folder into .git/hooks/pre-commit ```shell cp hooks/* .git/hooks/ @@ -18,4 +18,9 @@ cp hooks/* .git/hooks/ ### Common commands - - `cargo add ...` - Add a dependency to the Cargo.toml file \ No newline at end of file + - `cargo add ...` - Add a dependency to the Cargo.toml file + + +### Testing + +By default `cargo test` will run all the tests. If you want to exclude the expensive integration tests you can instead run `cargo test --bin unleash-edge`. \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml index e124ad5b..94862dbf 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -22,6 +22,7 @@ dotenv = { version = "0.15.0", features = ["clap"] } opentelemetry = { version = "0.18.0", features = ["trace", "rt-tokio", "metrics"] } opentelemetry-prometheus = "0.11.0" prometheus = { version = "0.13.3", features = ["process"] } +redis = "0.22.3" rustls = "0.20.8" rustls-pemfile = "1.0.2" serde = { version = "1.0.152", features = ["derive"] } @@ -37,6 +38,7 @@ unleash-yggdrasil = "0.4.2" [dev-dependencies] env_logger = "0.10.0" test-case = "2.2.2" +testcontainers = "0.14.0" [build-dependencies] shadow-rs = "0.20.0" diff --git a/server/src/cli.rs b/server/src/cli.rs index 724fd790..a6498e44 100644 --- a/server/src/cli.rs +++ b/server/src/cli.rs @@ -1,10 +1,41 @@ use std::path::PathBuf; -use clap::{Args, Parser, Subcommand}; +use clap::{ArgGroup, Args, Parser, Subcommand}; #[derive(Subcommand, Debug, Clone)] pub enum EdgeMode { Offline(OfflineArgs), + Edge(EdgeArgs), +} + +pub enum EdgeArg { + Redis(String), + Dynamo(String), +} + +impl From for EdgeArg { + fn from(value: EdgeArgs) -> Self { + if let Some(redis_url) = value.redis_url { + return EdgeArg::Redis(redis_url); + }; + if let Some(dynamo_url) = value.dynamo_url { + return EdgeArg::Dynamo(dynamo_url); + } + panic!("Unknown argument for edge type"); //This shouldn't be reachable without programmer error, that's what it's for + } +} + +#[derive(Args, Debug, Clone)] +#[command(group( + ArgGroup::new("data-provider") + .required(true) + .args(["redis_url", "dynamo_url"]), +))] +pub struct EdgeArgs { + #[clap(short, long, env)] + pub redis_url: Option, + #[clap(short, long, env)] + pub dynamo_url: Option, } #[derive(Args, Debug, Clone)] diff --git a/server/src/client_api.rs b/server/src/client_api.rs index 8648942a..c1e7748e 100644 --- a/server/src/client_api.rs +++ b/server/src/client_api.rs @@ -8,7 +8,7 @@ async fn features( edge_token: EdgeToken, features_source: web::Data, ) -> EdgeJsonResult { - let client_features = features_source.get_client_features(edge_token); + let client_features = features_source.get_client_features(edge_token)?; Ok(Json(client_features)) } diff --git a/server/src/data_sources/mod.rs b/server/src/data_sources/mod.rs new file mode 100644 index 00000000..1f519100 --- /dev/null +++ b/server/src/data_sources/mod.rs @@ -0,0 +1,2 @@ +pub mod redis_provider; +pub mod offline_provider; \ No newline at end of file diff --git a/server/src/offline_provider.rs b/server/src/data_sources/offline_provider.rs similarity index 72% rename from server/src/offline_provider.rs rename to server/src/data_sources/offline_provider.rs index 62629678..93ef72ff 100644 --- a/server/src/offline_provider.rs +++ b/server/src/data_sources/offline_provider.rs @@ -1,5 +1,5 @@ use crate::error::EdgeError; -use crate::types::{EdgeProvider, EdgeToken, FeaturesProvider, TokenProvider}; +use crate::types::{EdgeProvider, EdgeResult, EdgeToken, FeaturesProvider, TokenProvider}; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; @@ -12,25 +12,26 @@ pub struct OfflineProvider { } impl FeaturesProvider for OfflineProvider { - fn get_client_features(&self, _: EdgeToken) -> ClientFeatures { - self.features.clone() + fn get_client_features(&self, _: EdgeToken) -> Result { + Ok(self.features.clone()) } } impl TokenProvider for OfflineProvider { - fn get_known_tokens(&self) -> Vec { - self.valid_tokens.clone() + fn get_known_tokens(&self) -> EdgeResult> { + Ok(self.valid_tokens.clone()) } - fn secret_is_valid(&self, secret: &str) -> bool { - self.valid_tokens.iter().any(|t| t.secret == secret) + fn secret_is_valid(&self, secret: &str) -> EdgeResult { + Ok(self.valid_tokens.iter().any(|t| t.secret == secret)) } - fn token_details(&self, secret: String) -> Option { - self.valid_tokens + fn token_details(&self, secret: String) -> EdgeResult> { + Ok(self + .valid_tokens .clone() .into_iter() - .find(|t| t.secret == secret) + .find(|t| t.secret == secret)) } } diff --git a/server/src/data_sources/redis_provider.rs b/server/src/data_sources/redis_provider.rs new file mode 100644 index 00000000..00404732 --- /dev/null +++ b/server/src/data_sources/redis_provider.rs @@ -0,0 +1,58 @@ +use std::{sync::RwLock}; + +use redis::{Client, Commands, RedisError}; +use unleash_types::client_features::ClientFeatures; + +pub const FEATURE_KEY: &str = "features"; +pub const TOKENS_KEY: &str = "tokens"; + +use crate::{ + error::EdgeError, + types::{EdgeResult, EdgeToken, FeaturesProvider, TokenProvider, EdgeProvider}, +}; + +pub struct RedisProvider { + client: RwLock, +} + +impl From for EdgeError { + fn from(err: RedisError) -> Self { + EdgeError::DataSourceError(format!("Error connecting to Redis: {err}")) + } +} + +impl RedisProvider { + pub fn new(url: &str) -> Result { + let client = redis::Client::open(url)?; + Ok(Self { + client: RwLock::new(client), + }) + } +} + +impl EdgeProvider for RedisProvider {} + +impl FeaturesProvider for RedisProvider { + fn get_client_features(&self, _token: EdgeToken) -> EdgeResult { + let mut client = self.client.write().unwrap(); + let client_features: String = client.get(FEATURE_KEY)?; + serde_json::from_str::(&client_features).map_err(EdgeError::from) + } +} + +impl TokenProvider for RedisProvider { + fn get_known_tokens(&self) -> EdgeResult> { + let mut client = self.client.write().unwrap(); + let tokens: String = client.get(TOKENS_KEY)?; + serde_json::from_str::>(&tokens).map_err(EdgeError::from) + } + + fn secret_is_valid(&self, secret: &str) -> EdgeResult { + Ok(self.get_known_tokens()?.iter().any(|t| t.secret == secret)) + } + + fn token_details(&self, secret: String) -> EdgeResult> { + let tokens = self.get_known_tokens()?; + Ok(tokens.into_iter().find(|t| t.secret == secret)) + } +} diff --git a/server/src/edge_api.rs b/server/src/edge_api.rs index 69481962..2cc14927 100644 --- a/server/src/edge_api.rs +++ b/server/src/edge_api.rs @@ -15,8 +15,7 @@ async fn validate( .into_inner() .tokens .into_iter() - .filter(|t| token_provider.secret_is_valid(t)) - .map(|t| token_provider.token_details(t).unwrap()) // Guaranteed because we just checked that the secret exists + .filter_map(|t| token_provider.token_details(t).unwrap_or_default()) .collect(); Ok(Json(ValidatedTokens { tokens: valid_tokens, diff --git a/server/src/error.rs b/server/src/error.rs index e7bd6463..68a7ee97 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -11,6 +11,8 @@ pub enum EdgeError { NoTokenProvider, TokenParseError, TlsError, + DataSourceError(String), + JsonParseError(String), } impl Error for EdgeError {} @@ -20,14 +22,15 @@ impl Display for EdgeError { match self { EdgeError::InvalidBackupFile(path, why_invalid) => write!( f, - "file at path: {} was invalid due to {}", - path, why_invalid + "file at path: {path} was invalid due to {why_invalid}" ), EdgeError::TlsError => write!(f, "Could not configure TLS"), EdgeError::NoFeaturesFile => write!(f, "No features file located"), EdgeError::AuthorizationDenied => write!(f, "Not allowed to access"), EdgeError::NoTokenProvider => write!(f, "Could not get a TokenProvider"), EdgeError::TokenParseError => write!(f, "Could not parse edge token"), + EdgeError::DataSourceError(msg) => write!(f, "{msg}"), + EdgeError::JsonParseError(msg) => write!(f, "{msg}"), } } } @@ -41,6 +44,8 @@ impl ResponseError for EdgeError { EdgeError::AuthorizationDenied => StatusCode::FORBIDDEN, EdgeError::NoTokenProvider => StatusCode::INTERNAL_SERVER_ERROR, EdgeError::TokenParseError => StatusCode::UNAUTHORIZED, + EdgeError::DataSourceError(_) => StatusCode::INTERNAL_SERVER_ERROR, + EdgeError::JsonParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -49,5 +54,11 @@ impl ResponseError for EdgeError { } } +impl From for EdgeError { + fn from(value: serde_json::Error) -> Self { + EdgeError::JsonParseError(value.to_string()) + } +} + #[cfg(test)] mod tests {} diff --git a/server/src/frontend_api.rs b/server/src/frontend_api.rs index 4677cdc7..40b1af87 100644 --- a/server/src/frontend_api.rs +++ b/server/src/frontend_api.rs @@ -19,7 +19,7 @@ async fn get_frontend_features( let client_features = features_source.get_client_features(edge_token); let context = context.into_inner(); - let toggles = resolve_frontend_features(client_features, context).collect(); + let toggles = resolve_frontend_features(client_features?, context).collect(); Ok(Json(FrontendResult { toggles })) } @@ -33,7 +33,7 @@ async fn post_frontend_features( let client_features = features_source.get_client_features(edge_token); let context = context.into_inner(); - let toggles = resolve_frontend_features(client_features, context).collect(); + let toggles = resolve_frontend_features(client_features?, context).collect(); Ok(Json(FrontendResult { toggles })) } @@ -47,7 +47,7 @@ async fn get_enabled_frontend_features( let client_features = features_source.get_client_features(edge_token); let context = context.into_inner(); - let toggles: Vec = resolve_frontend_features(client_features, context) + let toggles: Vec = resolve_frontend_features(client_features?, context) .filter(|toggle| toggle.enabled) .collect(); @@ -63,7 +63,7 @@ async fn post_enabled_frontend_features( let client_features = features_source.get_client_features(edge_token); let context = context.into_inner(); - let toggles: Vec = resolve_frontend_features(client_features, context) + let toggles: Vec = resolve_frontend_features(client_features?, context) .filter(|toggle| toggle.enabled) .collect(); @@ -103,7 +103,7 @@ pub fn configure_frontend_api(cfg: &mut web::ServiceConfig) { mod tests { use std::sync::Arc; - use crate::types::{EdgeProvider, FeaturesProvider, TokenProvider}; + use crate::types::{EdgeProvider, EdgeResult, FeaturesProvider, TokenProvider, EdgeToken}; use actix_web::{ http::header::ContentType, test, @@ -130,24 +130,28 @@ mod tests { } impl FeaturesProvider for MockDataSource { - fn get_client_features(&self, _token: crate::types::EdgeToken) -> ClientFeatures { - self.features + fn get_client_features( + &self, + _token: crate::types::EdgeToken, + ) -> EdgeResult { + Ok(self + .features .as_ref() .expect("You need to populate the mock data for your test") - .clone() + .clone()) } } impl TokenProvider for MockDataSource { - fn get_known_tokens(&self) -> Vec { + fn get_known_tokens(&self) -> EdgeResult> { todo!() } - fn secret_is_valid(&self, _secret: &str) -> bool { - true + fn secret_is_valid(&self, _secret: &str) -> EdgeResult { + Ok(true) } - fn token_details(&self, _secret: String) -> Option { + fn token_details(&self, _secret: String) -> EdgeResult> { todo!() } } diff --git a/server/src/internal_backstage.rs b/server/src/internal_backstage.rs index cc8fa712..06eb97ca 100644 --- a/server/src/internal_backstage.rs +++ b/server/src/internal_backstage.rs @@ -26,7 +26,7 @@ pub async fn health() -> EdgeJsonResult { #[get("/info")] pub async fn info() -> EdgeJsonResult { - let data = BuildInfo::new(); + let data = BuildInfo::default(); Ok(Json(data)) } diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 00000000..41d14269 --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,11 @@ +pub mod data_sources; + +pub mod cli; +pub mod client_api; +pub mod edge_api; +pub mod error; +pub mod frontend_api; +pub mod metrics; +pub mod types; + +pub mod internal_backstage; diff --git a/server/src/main.rs b/server/src/main.rs index a479a511..c9ed8d6f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,7 +1,7 @@ use std::sync::Arc; + use crate::cli::EdgeMode; -use crate::offline_provider::OfflineProvider; use actix_cors::Cors; use actix_middleware_etag::Etag; use actix_web::{http, middleware, web, App, HttpServer}; @@ -10,16 +10,47 @@ use clap::Parser; use cli::CliArgs; use types::EdgeProvider; -mod cli; -mod client_api; -mod edge_api; -mod error; -mod frontend_api; -mod internal_backstage; -mod metrics; -mod offline_provider; +use unleash_edge::cli; +use unleash_edge::cli::EdgeArg; +use unleash_edge::cli::OfflineArgs; +use unleash_edge::client_api; +use unleash_edge::data_sources::offline_provider::OfflineProvider; +use unleash_edge::data_sources::redis_provider::RedisProvider; +use unleash_edge::edge_api; +use unleash_edge::frontend_api; +use unleash_edge::internal_backstage; +use unleash_edge::metrics; +use unleash_edge::types; +use unleash_edge::types::EdgeResult; + mod tls; -mod types; + +fn build_offline(offline_args: OfflineArgs) -> EdgeResult> { + Ok( + OfflineProvider::instantiate_provider( + offline_args.bootstrap_file, + offline_args.client_keys, + ) + .map(Arc::new)?, + ) +} + +fn build_redis(redis_url: String) -> EdgeResult> { + Ok(RedisProvider::new(&redis_url).map(Arc::new)?) +} + +fn build_data_source(args: CliArgs) -> EdgeResult> { + match args.mode { + EdgeMode::Offline(offline_args) => build_offline(offline_args), + EdgeMode::Edge(edge_args) => { + let arg: EdgeArg = edge_args.into(); + match arg { + EdgeArg::Redis(redis_url) => build_redis(redis_url), + EdgeArg::Dynamo(_) => todo!(), + } + } + } +} #[actix_web::main] async fn main() -> Result<(), anyhow::Error> { @@ -27,16 +58,10 @@ async fn main() -> Result<(), anyhow::Error> { let args = CliArgs::parse(); let http_args = args.clone().http; let (metrics_handler, request_metrics) = metrics::instantiate(None); - let client_provider = match args.mode { - EdgeMode::Offline(offline_args) => OfflineProvider::instantiate_provider( - offline_args.bootstrap_file, - offline_args.client_keys, - ), - } - .map_err(anyhow::Error::new)?; + let client_provider: Arc = + build_data_source(args).map_err(anyhow::Error::new)?; let server = HttpServer::new(move || { - let client_provider_arc: Arc = Arc::new(client_provider.clone()); - let client_provider_data = web::Data::from(client_provider_arc); + let client_provider_data = web::Data::from(client_provider.clone()); let cors_middleware = Cors::default() .allow_any_origin() diff --git a/server/src/metrics.rs b/server/src/metrics.rs index 37bb8429..1160a49d 100644 --- a/server/src/metrics.rs +++ b/server/src/metrics.rs @@ -5,11 +5,9 @@ use opentelemetry::{ export::metrics::aggregation, metrics::{controllers, processors, selectors}, }, - trace::FutureExt, }; #[cfg(target_os = "linux")] use prometheus::process_collector::ProcessCollector; -use tracing::instrument::WithSubscriber; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::{EnvFilter, Registry}; diff --git a/server/src/tls.rs b/server/src/tls.rs index 969becb9..b2a18672 100644 --- a/server/src/tls.rs +++ b/server/src/tls.rs @@ -3,7 +3,8 @@ use std::{fs::File, io::BufReader}; use rustls::{Certificate, PrivateKey, ServerConfig}; use rustls_pemfile::{certs, pkcs8_private_keys}; -use crate::{cli::TlsOptions, error::EdgeError}; +use crate::{cli::TlsOptions}; +use unleash_edge::error::EdgeError; pub(crate) fn config(tls_config: TlsOptions) -> Result { let config = ServerConfig::builder() diff --git a/server/src/types.rs b/server/src/types.rs index 97732fe3..f26068e5 100644 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -64,7 +64,7 @@ impl FromRequest for EdgeToken { None => Err(EdgeError::AuthorizationDenied), } .and_then(|client_token| { - if token_provider.secret_is_valid(&client_token.secret) { + if token_provider.secret_is_valid(&client_token.secret)? { Ok(client_token) } else { Err(EdgeError::AuthorizationDenied) @@ -149,16 +149,16 @@ pub struct ValidatedTokens { } pub trait FeaturesProvider { - fn get_client_features(&self, token: EdgeToken) -> ClientFeatures; + fn get_client_features(&self, token: EdgeToken) -> EdgeResult; } pub trait TokenProvider { - fn get_known_tokens(&self) -> Vec; - fn secret_is_valid(&self, secret: &str) -> bool; - fn token_details(&self, secret: String) -> Option; + fn get_known_tokens(&self) -> EdgeResult>; + fn secret_is_valid(&self, secret: &str) -> EdgeResult; + fn token_details(&self, secret: String) -> EdgeResult>; } -pub trait EdgeProvider: FeaturesProvider + TokenProvider {} +pub trait EdgeProvider: FeaturesProvider + TokenProvider + Send + Sync {} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BuildInfo { @@ -180,8 +180,8 @@ pub struct BuildInfo { } shadow!(build); // Get build information set to build placeholder -impl BuildInfo { - pub fn new() -> Self { +impl Default for BuildInfo { + fn default() -> Self { BuildInfo { package_version: build::PKG_VERSION.into(), app_name: build::PROJECT_NAME.into(), diff --git a/server/tests/redis_test.rs b/server/tests/redis_test.rs new file mode 100644 index 00000000..3079992e --- /dev/null +++ b/server/tests/redis_test.rs @@ -0,0 +1,32 @@ +use std::fs; + +use redis::Commands; +use testcontainers::{clients, images}; +use unleash_edge::{ + data_sources::redis_provider::{RedisProvider, FEATURE_KEY}, + types::{EdgeProvider, EdgeToken}, +}; + +#[tokio::test] +async fn redis_provider_returns_expected_data() { + let docker = clients::Cli::default(); + let node = docker.run(images::redis::Redis::default()); + let host_port = node.get_host_port_ipv4(6379); + let url = format!("redis://127.0.0.1:{host_port}"); + + let mut client = redis::Client::open(url.clone()).unwrap(); + + let content = + fs::read_to_string("../examples/features.json".to_string()).expect("Could not read file"); + + //Wants a type annotation but we don't care about the result so we immediately discard the data coming back + let _: () = client.set(FEATURE_KEY, content).unwrap(); + + let provider: Box = Box::new(RedisProvider::new(&url).unwrap()); + + let features = provider + .get_client_features(EdgeToken::try_from("secret-123".to_string()).unwrap()) + .unwrap(); + + assert!(!features.features.is_empty()); +}