diff --git a/services/.env b/services/.env index 5b6867f51..4a5a0de60 100644 --- a/services/.env +++ b/services/.env @@ -1,5 +1,6 @@ COMM_SERVICES_PORT_TUNNELBROKER=50051 COMM_SERVICES_PORT_BACKUP=50052 COMM_SERVICES_PORT_BLOB=50053 COMM_SERVICES_PORT_IDENTITY=50054 +COMM_SERVICES_PORT_FEATURE_FLAGS=50055 COMM_TEST_SERVICES=0 diff --git a/services/docker-compose.yml b/services/docker-compose.yml index 1a7a31f65..334a7a691 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -1,90 +1,102 @@ version: '3.9' volumes: localstack: services: # tunnelbroker tunnelbroker-server: depends_on: - localstack - rabbitmq build: dockerfile: services/tunnelbroker/Dockerfile context: ../ args: - COMM_TEST_SERVICES=${COMM_TEST_SERVICES} - COMM_SERVICES_SANDBOX=${COMM_SERVICES_SANDBOX} image: commapp/tunnelbroker-server:0.2 ports: - '${COMM_SERVICES_PORT_TUNNELBROKER}:50051' volumes: - $HOME/.aws/config:/root/.aws/config:ro - $HOME/.aws/credentials:/root/.aws/credentials:ro - ./tunnelbroker/tunnelbroker.ini:/root/tunnelbroker/tunnelbroker.ini:ro - ./tunnelbroker/tunnelbroker-sandbox.ini:/root/tunnelbroker/tunnelbroker-sandbox.ini:ro # backup backup-server: depends_on: - localstack - blob-server build: dockerfile: services/backup/Dockerfile context: ../ args: - COMM_TEST_SERVICES=${COMM_TEST_SERVICES} - COMM_SERVICES_SANDBOX=${COMM_SERVICES_SANDBOX} - LOCALSTACK_URL=http://localstack:4566 - BLOB_SERVICE_URL=http://blob-server:50051 image: commapp/backup-server:0.1 ports: - '${COMM_SERVICES_PORT_BACKUP}:50051' volumes: - $HOME/.aws/credentials:/home/comm/.aws/credentials:ro # blob blob-server: depends_on: - localstack build: dockerfile: services/blob/Dockerfile context: ../ args: - COMM_TEST_SERVICES=${COMM_TEST_SERVICES} - COMM_SERVICES_SANDBOX=${COMM_SERVICES_SANDBOX} image: commapp/blob-server:0.1 ports: - '${COMM_SERVICES_PORT_BLOB}:50051' volumes: - $HOME/.aws/config:/home/comm/.aws/config:ro - $HOME/.aws/credentials:/home/comm/.aws/credentials:ro # identity identity-server: depends_on: - localstack build: dockerfile: services/identity/Dockerfile context: ../ image: commapp/identity-server:0.1 ports: - '${COMM_SERVICES_PORT_IDENTITY}:50051' + feature-flags-server: + depends_on: + - localstack + build: + dockerfile: services/feature-flags/Dockerfile + context: ../ + image: commapp/feature-flags:0.1 + ports: + - '${COMM_SERVICES_PORT_FEATURE_FLAGS}:50051' + volumes: + - $HOME/.aws/config:/home/comm/.aws/config:ro + - $HOME/.aws/credentials:/home/comm/.aws/credentials:ro # localstack localstack: image: localstack/localstack hostname: localstack ports: - '4566:4566' environment: - SERVICES=s3,dynamodb - DATA_DIR=/tmp/localstack - HOSTNAME_EXTERNAL=localstack volumes: - localstack:/tmp/localstack # RabbitMQ rabbitmq: image: rabbitmq:3-management hostname: rabbitmq ports: - '5672:5672' - '5671:5671' - '15672:15672' environment: - RABBITMQ_DEFAULT_USER=comm - RABBITMQ_DEFAULT_PASS=comm diff --git a/services/feature-flags/Dockerfile b/services/feature-flags/Dockerfile new file mode 100644 index 000000000..84bcbd889 --- /dev/null +++ b/services/feature-flags/Dockerfile @@ -0,0 +1,44 @@ +FROM commapp/services-base:1.3.2 as builder + +ENV PATH=/root/.cargo/bin:$PATH + +# Install Curl +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Rust +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y + +RUN mkdir -p /home/comm/app/feature-flags +WORKDIR /home/comm/app/feature-flags +RUN cargo init --bin + +# Cache build dependencies in a new layer +COPY services/blob/Cargo.toml services/blob/Cargo.lock ./ +COPY services/comm-services-lib ../comm-services-lib +RUN cargo build --release + +# Copy actual application sources +COPY services/feature-flags . + +# Remove the previously-built binary so that only the application itself is +# rebuilt +RUN rm -f ./target/release/deps/feature-flags* +RUN cargo build --release + +# Runner stage +FROM commapp/services-base:1.3.2 as runner + +# Create a new user comm and use it to run subsequent commands +RUN useradd -m comm +USER comm + +# Only copy built binary from builder stage +WORKDIR /home/comm/app/feature-flags +COPY --from=builder /home/comm/app/feature-flags/target/release/feature-flags . + +ARG COMM_SERVICES_SANDBOX +ENV COMM_SERVICES_SANDBOX=${COMM_SERVICES_SANDBOX} + +CMD ./feature-flags diff --git a/services/feature-flags/src/config.rs b/services/feature-flags/src/config.rs index 537b56172..fd10569f3 100644 --- a/services/feature-flags/src/config.rs +++ b/services/feature-flags/src/config.rs @@ -1,46 +1,47 @@ use aws_sdk_dynamodb::{Endpoint, Region}; use clap::{builder::FalseyValueParser, Parser}; use http::Uri; use once_cell::sync::Lazy; use tracing::info; use crate::constants::{ - AWS_REGION, DEFAULT_LOCALSTACK_URL, HTTP_SERVER_DEFAULT_PORT, + AWS_REGION, DEFAULT_LOCALSTACK_URL, HTTP_SERVER_DEFAULT_PORT, SANDBOX_ENV_VAR, }; #[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)] pub http_port: u16, } 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().region(Region::new(AWS_REGION)); if CONFIG.is_sandbox { info!( "Running in sandbox environment. Localstack URL: {}", &CONFIG.localstack_url ); config_builder = config_builder.endpoint_resolver(Endpoint::immutable( Uri::from_static(&CONFIG.localstack_url), )); } config_builder.load().await } diff --git a/services/feature-flags/src/constants.rs b/services/feature-flags/src/constants.rs index df3d2f99d..fb1181e89 100644 --- a/services/feature-flags/src/constants.rs +++ b/services/feature-flags/src/constants.rs @@ -1,31 +1,32 @@ pub const AWS_REGION: &str = "us-east-2"; 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"; // 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 c3da8de84..92d48b171 100644 --- a/services/feature-flags/src/service.rs +++ b/services/feature-flags/src/service.rs @@ -1,111 +1,111 @@ use crate::config::CONFIG; use crate::constants::{PLATFORM_ANDROID, PLATFORM_IOS}; use crate::database::{DatabaseClient, FeatureConfig, Platform}; use actix_web::http::header::ContentType; use actix_web::{web, App, HttpResponse, HttpServer}; use comm_services_lib::database::Error; use serde::Deserialize; use std::collections::HashSet; 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<()> { 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(("localhost", CONFIG.http_port))? + .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 = features.into_iter().collect::>().join(","); HttpResponse::Ok() .content_type(ContentType::plaintext()) .body(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(Deserialize, Debug)] struct FeatureQuery { code_version: i32, is_staff: bool, platform: String, } diff --git a/services/package.json b/services/package.json index 24f311544..556b95af8 100644 --- a/services/package.json +++ b/services/package.json @@ -1,26 +1,28 @@ { "name": "services", "version": "1.0.0", "private": true, "license": "BSD-3-Clause", "scripts": { "build-all": "./scripts/build_base_image.sh && docker-compose build", "build-base-image": "./scripts/build_base_image.sh", "run-tunnelbroker-service": "./scripts/run_server_image.sh tunnelbroker", "run-tunnelbroker-service-in-sandbox": "COMM_SERVICES_SANDBOX=1 ./scripts/run_server_image.sh tunnelbroker", "build-backup-base": "./scripts/build_base_image.sh && docker-compose build backup-base", "run-backup-service": "./scripts/run_server_image.sh backup", "run-backup-service-in-sandbox": "COMM_SERVICES_SANDBOX=1 ./scripts/run_server_image.sh backup", "build-blob-base": "./scripts/build_base_image.sh && docker-compose build blob-base", "run-blob-service": "./scripts/run_server_image.sh blob", "run-blob-service-in-sandbox": "COMM_SERVICES_SANDBOX=1 ./scripts/run_server_image.sh blob", + "run-feature-flags-service": "./scripts/run_server_image.sh feature-flags", + "run-feature-flags-service-in-sandbox": "COMM_SERVICES_SANDBOX=1 ./scripts/run_server_image.sh feature-flags", "run-all-services": "./scripts/run_all_services.sh", "run-unit-tests": "./scripts/run_unit_tests.sh", "run-integration-tests": "./scripts/run_integration_tests.sh", "run-performance-tests": "./scripts/run_performance_tests.sh", "run-all-services-in-sandbox": "COMM_SERVICES_SANDBOX=1 ./scripts/run_all_services.sh", "init-local-cloud": "./scripts/init_local_cloud.sh", "delete-local-cloud": "docker-compose down -v", "reset-local-cloud": "yarn delete-local-cloud && yarn init-local-cloud" } } diff --git a/services/scripts/run_server_image.sh b/services/scripts/run_server_image.sh index 1448ffe3f..63e0981fa 100755 --- a/services/scripts/run_server_image.sh +++ b/services/scripts/run_server_image.sh @@ -1,33 +1,37 @@ #!/usr/bin/env bash set -e if [[ "$#" -lt 1 ]] || [[ "$#" -gt 2 ]]; then echo "Illegal number of arguments, expected 2:" echo "- one argument with a name of the service, currently available services:" ./scripts/list_services.sh echo "- one optional argument with port" echo "- example: ./services/scripts/run_server_image.sh tunnelbroker 12345" exit 1; fi SERVICE=$1 if [[ "$SERVICE" == "tunnelbroker" ]]; then if [[ -n "$2" ]]; then export COMM_SERVICES_PORT_TUNNELBROKER=$2 fi elif [[ "$SERVICE" == "backup" ]]; then if [[ -n "$2" ]]; then export COMM_SERVICES_PORT_BACKUP=$2 fi elif [[ "$SERVICE" == "blob" ]]; then if [[ -n "$2" ]]; then export COMM_SERVICES_PORT_BLOB=$2 fi +elif [[ "$SERVICE" == "feature-flags" ]]; then + if [[ -n "$2" ]]; then + export COMM_SERVICES_PORT_FEATURE_FLAGS=$2 + fi else echo "No such service ${SERVICE}, aborting" exit 1 fi docker-compose build "$SERVICE"-server docker-compose up "$SERVICE"-server