diff --git a/tembo-cli/src/cli/tembo_config.rs b/tembo-cli/src/cli/tembo_config.rs index c076db606..8fc427276 100644 --- a/tembo-cli/src/cli/tembo_config.rs +++ b/tembo-cli/src/cli/tembo_config.rs @@ -32,6 +32,18 @@ pub struct InstanceSettings { pub extra_domains_rw: Option>, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct OverlayInstanceSettings { + pub cpu: Option, + pub memory: Option, + pub storage: Option, + pub replicas: Option, + pub stack_type: Option, + pub postgres_configurations: Option>, + pub extensions: Option>, + pub extra_domains_rw: Option>, +} + // If a trunk project name is not specified, then assume // it's the same name as the extension. fn deserialize_extensions<'de, D>( diff --git a/tembo-cli/src/cmd/apply.rs b/tembo-cli/src/cmd/apply.rs index 5e4198227..af7e73b74 100644 --- a/tembo-cli/src/cmd/apply.rs +++ b/tembo-cli/src/cmd/apply.rs @@ -1,6 +1,7 @@ +use anyhow::Context as AnyhowContext; use anyhow::Error; use clap::Args; -use colorful::{Color, Colorful}; +use colorful::Colorful; use controller::stacks::get_stack; use controller::stacks::types::StackType as ControllerStackType; use log::info; @@ -29,7 +30,8 @@ use crate::cli::file_utils::FileUtils; use crate::cli::sqlx_utils::SqlxUtils; use crate::cli::tembo_config; use crate::cli::tembo_config::InstanceSettings; -use crate::tui::{indent, instance_started}; +use crate::cli::tembo_config::OverlayInstanceSettings; +use crate::tui::instance_started; use crate::{ cli::context::{get_current_context, Environment, Profile, Target}, tui::{clean_console, colors, white_confirmation}, @@ -41,9 +43,12 @@ const POSTGRESCONF_NAME: &str = "postgres.conf"; /// Deploys a tembo.toml file #[derive(Args)] -pub struct ApplyCommand {} +pub struct ApplyCommand { + #[clap(long, short = 'm')] + pub merge: Option, +} -pub fn execute(verbose: bool) -> Result<(), anyhow::Error> { +pub fn execute(verbose: bool, _merge_path: Option) -> Result<(), anyhow::Error> { info!("Running validation!"); super::validate::execute(verbose)?; info!("Validation completed!"); @@ -51,18 +56,18 @@ pub fn execute(verbose: bool) -> Result<(), anyhow::Error> { let env = get_current_context()?; if env.target == Target::Docker.to_string() { - return execute_docker(verbose); + return execute_docker(verbose, _merge_path); } else if env.target == Target::TemboCloud.to_string() { - return execute_tembo_cloud(env.clone()); + return execute_tembo_cloud(env.clone(), _merge_path); } Ok(()) } -fn execute_docker(verbose: bool) -> Result<(), anyhow::Error> { +fn execute_docker(verbose: bool, _merge_path: Option) -> Result<(), anyhow::Error> { Docker::installed_and_running()?; - let instance_settings: HashMap = get_instance_settings()?; + let instance_settings = get_instance_settings(_merge_path)?; let rendered_dockerfile: String = get_rendered_dockerfile(instance_settings.clone())?; FileUtils::create_file( @@ -111,13 +116,17 @@ fn execute_docker(verbose: bool) -> Result<(), anyhow::Error> { &value.stack_type, "local", ); + println!("Instance settings: {:?}", instance_settings); } Ok(()) } -pub fn execute_tembo_cloud(env: Environment) -> Result<(), anyhow::Error> { - let instance_settings: HashMap = get_instance_settings()?; +pub fn execute_tembo_cloud( + env: Environment, + _merge_path: Option, +) -> Result<(), anyhow::Error> { + let instance_settings = get_instance_settings(_merge_path)?; let profile = env.clone().selected_profile.unwrap(); let config = Configuration { @@ -434,25 +443,55 @@ fn get_trunk_installs( vec_trunk_installs } -pub fn get_instance_settings() -> Result, anyhow::Error> { - let mut file_path = FileUtils::get_current_working_dir(); - file_path.push_str("/tembo.toml"); - - let contents = match fs::read_to_string(file_path.clone()) { - Ok(c) => c, - Err(e) => { - panic!("Couldn't read context file {}: {}", file_path, e); - } - }; +fn merge_settings(base: &InstanceSettings, overlay: OverlayInstanceSettings) -> InstanceSettings { + InstanceSettings { + environment: base.environment.clone(), // Retain the base environment + instance_name: base.instance_name.clone(), // Retain the base instance_name + cpu: overlay.cpu.unwrap_or_else(|| base.cpu.clone()), + memory: overlay.memory.unwrap_or_else(|| base.memory.clone()), + storage: overlay.storage.unwrap_or_else(|| base.storage.clone()), + replicas: overlay.replicas.unwrap_or(base.replicas), + stack_type: overlay + .stack_type + .unwrap_or_else(|| base.stack_type.clone()), + postgres_configurations: overlay + .postgres_configurations + .or_else(|| base.postgres_configurations.clone()), + extensions: overlay.extensions.or_else(|| base.extensions.clone()), + extra_domains_rw: overlay + .extra_domains_rw + .or_else(|| base.extra_domains_rw.clone()), + } +} - let instance_settings: HashMap = match toml::from_str(&contents) { - Ok(d) => d, - Err(e) => { - panic!("Unable to load data. Error: `{}`", e); +pub fn get_instance_settings( + overlay_file_path: Option, +) -> Result, Error> { + let mut base_path = FileUtils::get_current_working_dir(); + base_path.push_str("/tembo.toml"); + let base_contents = fs::read_to_string(&base_path) + .with_context(|| format!("Couldn't read base file {}", base_path))?; + let base_settings: HashMap = + toml::from_str(&base_contents).context("Unable to load data from the base config")?; + + let mut final_settings = base_settings.clone(); + + if let Some(overlay_path) = overlay_file_path { + let overlay_contents = fs::read_to_string(&overlay_path) + .with_context(|| format!("Couldn't read overlay file {}", overlay_path))?; + let overlay_settings: HashMap = + toml::from_str(&overlay_contents) + .context("Unable to load data from the overlay config")?; + + for (key, overlay_value) in overlay_settings { + if let Some(base_value) = base_settings.get(&key) { + let merged_value = merge_settings(base_value, overlay_value); + final_settings.insert(key, merged_value); + } } - }; + } - Ok(instance_settings) + Ok(final_settings) } pub fn get_rendered_dockerfile( @@ -557,7 +596,6 @@ fn get_postgres_config(instance_settings: HashMap) -> } } } - postgres_config } diff --git a/tembo-cli/src/cmd/context/list.rs b/tembo-cli/src/cmd/context/list.rs index 5ad8add71..5f4af3adb 100644 --- a/tembo-cli/src/cmd/context/list.rs +++ b/tembo-cli/src/cmd/context/list.rs @@ -15,7 +15,7 @@ pub fn execute() -> Result<(), anyhow::Error> { org_id = env_org; } - if e.target == String::from("docker") { + if e.target == *"docker" { profile = String::from("local") } else if let Some(env_profile) = e.profile { profile = env_profile; diff --git a/tembo-cli/src/cmd/delete.rs b/tembo-cli/src/cmd/delete.rs index 06bc6a67b..d89804bf3 100644 --- a/tembo-cli/src/cmd/delete.rs +++ b/tembo-cli/src/cmd/delete.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; - use crate::cli::context::{get_current_context, Environment, Target}; use crate::cli::docker::Docker; -use crate::cli::tembo_config::InstanceSettings; -use crate::tui::{confirmation, label}; + +use crate::tui::confirmation; use clap::Args; use core::result::Result::Ok; use temboclient::apis::{configuration::Configuration, instance_api::delete_instance}; @@ -18,7 +16,7 @@ pub struct DeleteCommand {} pub fn execute() -> Result<(), anyhow::Error> { let env = get_current_context()?; - let instance_settings: HashMap = get_instance_settings()?; + let instance_settings = get_instance_settings(None)?; if env.target == Target::Docker.to_string() { for (_key, value) in instance_settings.iter() { @@ -32,7 +30,7 @@ pub fn execute() -> Result<(), anyhow::Error> { } fn execute_tembo_cloud(env: Environment) -> Result<(), anyhow::Error> { - let instance_settings: HashMap = get_instance_settings()?; + let instance_settings = get_instance_settings(None)?; let profile = env.clone().selected_profile.unwrap(); let config = Configuration { diff --git a/tembo-cli/src/cmd/init.rs b/tembo-cli/src/cmd/init.rs index 5e2b9f08b..a3f4786d5 100644 --- a/tembo-cli/src/cmd/init.rs +++ b/tembo-cli/src/cmd/init.rs @@ -5,7 +5,6 @@ use crate::cli::context::{ use crate::cli::file_utils::FileUtils; use crate::tui::confirmation; use clap::Args; -use colorful::Colorful; /// Initializes a local environment. Creates a sample context and configuration files. #[derive(Args)] diff --git a/tembo-cli/src/main.rs b/tembo-cli/src/main.rs index 4814d101e..fd7665e61 100644 --- a/tembo-cli/src/main.rs +++ b/tembo-cli/src/main.rs @@ -53,7 +53,7 @@ fn main() -> Result<(), anyhow::Error> { init::execute()?; } SubCommands::Apply(_apply_cmd) => { - apply::execute(app.global_opts.verbose)?; + apply::execute(app.global_opts.verbose, _apply_cmd.merge.clone())?; } SubCommands::Validate(_validate_cmd) => { validate::execute(app.global_opts.verbose)?; @@ -65,3 +65,89 @@ fn main() -> Result<(), anyhow::Error> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use std::process::Command; + + const CARGO_BIN_PATH: &str = "cargo run "; + const root_dir: &str = env!("CARGO_MANIFEST_DIR"); + + #[tokio::test] + async fn default_instance_settings() -> Result<(), Box> { + std::env::set_current_dir( + PathBuf::from(root_dir) + .join("tests") + .join("tomls") + .join("merge"), + )?; + + // Path to the overlay.toml file + let overlay_config_path = PathBuf::from(root_dir) + .join("tests") + .join("tomls") + .join("merge") + .join("overlay.toml"); + let overlay_config_str = overlay_config_path.to_str().ok_or("Invalid path")?; + + // Running `tembo init` + let _output = Command::new(CARGO_BIN_PATH).arg("init"); + + let _output = Command::new(CARGO_BIN_PATH) + .arg("apply") + .arg("--merge") + .arg(overlay_config_str); + + let merged_settings = apply::get_instance_settings(Some(overlay_config_str.to_string()))?; + if let Some(setting) = merged_settings.get("defaults") { + assert_ne!(setting.cpu, "0.25", "Default setting was overwritten"); + } else { + return Err("Setting key not found".into()); + } + + // Running `tembo delete` + let _output = Command::new(CARGO_BIN_PATH).arg("delete"); + + Ok(()) + } + + #[tokio::test] + async fn merge() -> Result<(), Box> { + std::env::set_current_dir( + PathBuf::from(root_dir) + .join("tests") + .join("tomls") + .join("merge"), + )?; + + // Path to the overlay.toml file + let overlay_config_path = PathBuf::from(root_dir) + .join("tests") + .join("tomls") + .join("merge") + .join("overlay.toml"); + let overlay_config_str = overlay_config_path.to_str().ok_or("Invalid path")?; + + // Running `tembo init` + let _output = Command::new(CARGO_BIN_PATH).arg("init"); + + let _output = Command::new(CARGO_BIN_PATH) + .arg("apply") + .arg("--merge") + .arg(overlay_config_str); + + let merged_settings = apply::get_instance_settings(Some(overlay_config_str.to_string()))?; + if let Some(setting) = merged_settings.get("defaults") { + assert_eq!(setting.memory, "10Gi", "Base settings was not overwritten"); + } else { + return Err("Setting key not found".into()); + } + + // Running `tembo delete` + let _output = Command::new(CARGO_BIN_PATH).arg("delete"); + + Ok(()) + } +} diff --git a/tembo-cli/src/tui.rs b/tembo-cli/src/tui.rs index e4d4e8938..f47227f36 100644 --- a/tembo-cli/src/tui.rs +++ b/tembo-cli/src/tui.rs @@ -100,7 +100,7 @@ pub mod colors { ColorfulRgb::new(255, 244, 228) } - pub fn gradient_p<'a>(log: &'a str) -> GradientDisplay<'a, [RGB; 4]> { + pub fn gradient_p(log: &str) -> GradientDisplay<'_, [RGB; 4]> { GradientStr::gradient( log, [ @@ -112,7 +112,7 @@ pub mod colors { ) } - pub fn gradient_rainbow<'a>(log: &'a str) -> GradientDisplay<'a, [RGB; 3]> { + pub fn gradient_rainbow(log: &str) -> GradientDisplay<'_, [RGB; 3]> { GradientStr::gradient( log, [ diff --git a/tembo-cli/tests/tomls/merge/.gitignore b/tembo-cli/tests/tomls/merge/.gitignore new file mode 100644 index 000000000..71c14668c --- /dev/null +++ b/tembo-cli/tests/tomls/merge/.gitignore @@ -0,0 +1,3 @@ +postgres.conf +1_extensions.sql +Dockerfile \ No newline at end of file diff --git a/tembo-cli/tests/tomls/merge/overlay.toml b/tembo-cli/tests/tomls/merge/overlay.toml new file mode 100644 index 000000000..5d884cd49 --- /dev/null +++ b/tembo-cli/tests/tomls/merge/overlay.toml @@ -0,0 +1,4 @@ +[defaults] +environment = "prod" +instance_name = "overlay_instance" +memory = "10Gi" \ No newline at end of file diff --git a/tembo-cli/tests/tomls/merge/tembo.toml b/tembo-cli/tests/tomls/merge/tembo.toml new file mode 100644 index 000000000..d3ada1af5 --- /dev/null +++ b/tembo-cli/tests/tomls/merge/tembo.toml @@ -0,0 +1,7 @@ +[defaults] +environment = "dev" +instance_name = "defaults_instance" +cpu = "1" +storage = "50Gi" +replicas = 1 +stack_type = "Standard" \ No newline at end of file