diff --git a/services/feature-flags/Dockerfile b/services/feature-flags/Dockerfile index 5fc66ffe7..a27b5dbe4 100644 --- a/services/feature-flags/Dockerfile +++ b/services/feature-flags/Dockerfile @@ -1,35 +1,36 @@ FROM rust:1.70-bullseye as builder RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ build-essential cmake git libgtest-dev libssl-dev zlib1g-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /home/root/app/feature-flags ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse # Copy actual application sources COPY services/comm-services-lib ../comm-services-lib COPY services/feature-flags ./ RUN cargo install --locked --path . # Runner stage FROM debian:bullseye-slim as runner # Update dependencies, install ca-certificates which are required for TLS RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ ca-certificates \ && rm -rf /var/lib/apt/lists/* # Only copy built binary from builder stage COPY --from=builder /usr/local/cargo/bin/feature-flags /usr/local/bin/feature-flags WORKDIR /home/comm/app/feature-flags # Create a new user comm and use it to run subsequent commands RUN useradd -m comm USER comm ENV RUST_LOG=info -CMD ["feature-flags"] +# For compatibility with the existing Terraform config +CMD ["feature-flags", "--http-port", "50051"] diff --git a/services/feature-flags/src/config.rs b/services/feature-flags/src/config.rs index c5d42ad1b..40d892e0a 100644 --- a/services/feature-flags/src/config.rs +++ b/services/feature-flags/src/config.rs @@ -1,42 +1,33 @@ -use clap::{builder::FalseyValueParser, Parser}; +use clap::Parser; use once_cell::sync::Lazy; use tracing::info; -use crate::constants::{ - DEFAULT_LOCALSTACK_URL, HTTP_SERVER_DEFAULT_PORT, SANDBOX_ENV_VAR, -}; +use crate::constants::HTTP_SERVER_DEFAULT_PORT; #[derive(Parser)] #[command(version, about, long_about = None)] pub struct AppConfig { - /// Run the service in sandbox - #[arg(long = "sandbox", default_value_t = false)] - #[arg(env = SANDBOX_ENV_VAR)] - #[arg(value_parser = FalseyValueParser::new())] - pub is_sandbox: bool, - /// AWS Localstack service URL, applicable in sandbox mode - #[arg(long, default_value_t = DEFAULT_LOCALSTACK_URL.to_string())] - pub localstack_url: String, - #[arg(long = "port", default_value_t = HTTP_SERVER_DEFAULT_PORT)] + /// AWS Localstack service URL + #[arg(env = "LOCALSTACK_ENDPOINT")] + #[arg(long)] + pub localstack_endpoint: Option, + #[arg(long, default_value_t = HTTP_SERVER_DEFAULT_PORT)] pub http_port: u16, } -pub static CONFIG: Lazy = Lazy::new(|| AppConfig::parse()); +pub static CONFIG: Lazy = Lazy::new(AppConfig::parse); pub fn parse_cmdline_args() { Lazy::force(&CONFIG); } pub async fn load_aws_config() -> aws_types::SdkConfig { let mut config_builder = aws_config::from_env(); - if CONFIG.is_sandbox { - info!( - "Running in sandbox environment. Localstack URL: {}", - &CONFIG.localstack_url - ); - config_builder = config_builder.endpoint_url(&CONFIG.localstack_url); + if let Some(endpoint) = &CONFIG.localstack_endpoint { + info!("Using Localstack. AWS endpoint URL: {}", endpoint); + config_builder = config_builder.endpoint_url(endpoint); } config_builder.load().await } diff --git a/services/feature-flags/src/constants.rs b/services/feature-flags/src/constants.rs index 28eb62ed3..0aae3b565 100644 --- a/services/feature-flags/src/constants.rs +++ b/services/feature-flags/src/constants.rs @@ -1,31 +1,29 @@ -pub const DEFAULT_LOCALSTACK_URL: &str = "http://localhost:4566"; pub const LOG_LEVEL_ENV_VAR: &str = tracing_subscriber::filter::EnvFilter::DEFAULT_ENV; -pub const HTTP_SERVER_DEFAULT_PORT: u16 = 50051; -pub const SANDBOX_ENV_VAR: &str = "COMM_SERVICES_SANDBOX"; +pub const HTTP_SERVER_DEFAULT_PORT: u16 = 50055; // The configuration of feature flags is stored in a table in DynamoDB. // Each row is identified by a compound primary key consisting of // partition key - platform and sort key - feature. // A row also contains the configuration, which is a map indexed by code // version with values containing boolean flags for staff and non-staff config. // A sample row from the db looks like this: // { // "feature": S("FEATURE_1"), // "configuration": M({ // "123": M({ // "staff": Bool(true), // "non-staff": Bool(false) // }) // }), // "platform": S("ANDROID") // } pub const FEATURE_FLAGS_TABLE_NAME: &str = "feature-flags"; pub const FEATURE_FLAGS_PLATFORM_FIELD: &str = "platform"; pub const FEATURE_FLAGS_CONFIG_FIELD: &str = "configuration"; pub const FEATURE_FLAGS_FEATURE_FIELD: &str = "feature"; pub const FEATURE_FLAGS_STAFF_FIELD: &str = "staff"; pub const FEATURE_FLAGS_NON_STAFF_FIELD: &str = "non-staff"; pub const PLATFORM_IOS: &str = "IOS"; pub const PLATFORM_ANDROID: &str = "ANDROID"; diff --git a/services/feature-flags/src/service.rs b/services/feature-flags/src/service.rs index 7085dec8f..e0a952924 100644 --- a/services/feature-flags/src/service.rs +++ b/services/feature-flags/src/service.rs @@ -1,115 +1,121 @@ use crate::config::CONFIG; use crate::constants::{PLATFORM_ANDROID, PLATFORM_IOS}; use crate::database::{DatabaseClient, FeatureConfig, Platform}; use actix_web::{web, App, HttpResponse, HttpServer}; use comm_services_lib::database::Error; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use tracing::info; pub struct FeatureFlagsService { db: DatabaseClient, } impl FeatureFlagsService { pub fn new(db_client: DatabaseClient) -> Self { FeatureFlagsService { db: db_client } } pub async fn start(&self) -> std::io::Result<()> { + info!( + "Starting HTTP server listening at port {}", + CONFIG.http_port + ); + let db_clone = self.db.clone(); HttpServer::new(move || { App::new() .app_data(web::Data::new(db_clone.to_owned())) .service( web::resource("/features") .route(web::get().to(Self::features_handler)), ) }) .bind(("0.0.0.0", CONFIG.http_port))? .run() .await } async fn features_handler( client: web::Data, query: web::Query, ) -> HttpResponse { let platform = match query.platform.as_str().to_uppercase().as_str() { PLATFORM_IOS => Platform::IOS, PLATFORM_ANDROID => Platform::ANDROID, _ => return HttpResponse::BadRequest().finish(), }; match Self::enabled_features_set( client.get_ref(), platform, query.code_version, query.is_staff, ) .await { Ok(features) => { let response_body = FeaturesResponse { enabled_features: features, }; HttpResponse::Ok().json(response_body) } _ => HttpResponse::InternalServerError().finish(), } } async fn enabled_features_set( db: &DatabaseClient, platform: Platform, code_version: i32, is_staff: bool, ) -> Result, Error> { let features_config = db.get_features_configuration(platform).await?; Ok( features_config .into_values() .filter_map(|config| { Self::feature_name_if_enabled(code_version, is_staff, config) }) .collect(), ) } fn feature_name_if_enabled( code_version: i32, is_staff: bool, feature_config: FeatureConfig, ) -> Option { feature_config .config .keys() .filter(|version| *version <= &code_version) .max() .and_then(|version| feature_config.config.get(version)) .map(|config| { if is_staff { config.staff } else { config.non_staff } }) .and_then(|is_enabled| { if is_enabled { Some(feature_config.name) } else { None } }) } } #[derive(Serialize)] struct FeaturesResponse { enabled_features: HashSet, } #[derive(Deserialize, Debug)] struct FeatureQuery { code_version: i32, is_staff: bool, platform: String, }