From 5a2196aa21ccedc25ec1d94e23ad700441df5638 Mon Sep 17 00:00:00 2001 From: Nick Hudson Date: Sun, 7 Jan 2024 17:08:55 -0600 Subject: [PATCH] Add validation of configuration values from the tembo.toml file (#471) --- tembo-cli/Cargo.lock | 61 +++++- tembo-cli/Cargo.toml | 3 +- tembo-cli/README.md | 133 ++++++------- tembo-cli/src/cli/tembo_config.rs | 4 +- tembo-cli/src/cmd/validate.rs | 222 +++++++++++++++++++++- tembo-cli/temboclient/src/models/impls.rs | 3 + 6 files changed, 356 insertions(+), 70 deletions(-) diff --git a/tembo-cli/Cargo.lock b/tembo-cli/Cargo.lock index 9d3956df5..bebdaf7ab 100644 --- a/tembo-cli/Cargo.lock +++ b/tembo-cli/Cargo.lock @@ -1345,6 +1345,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.29" @@ -1390,6 +1396,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "globset" version = "0.4.14" @@ -2402,6 +2414,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.2.1+3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.98" @@ -2410,6 +2431,7 @@ checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2986,6 +3008,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "reqwest" version = "0.11.20" @@ -3083,6 +3111,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.37", + "unicode-ident", +] + [[package]] name = "rtoolbox" version = "0.0.1" @@ -3751,7 +3808,7 @@ dependencies = [ [[package]] name = "tembo-cli" -version = "0.13.4" +version = "0.13.5" dependencies = [ "anyhow", "assert_cmd", @@ -3771,9 +3828,11 @@ dependencies = [ "jwt", "log", "mockall", + "openssl", "predicates 2.1.5", "reqwest", "rpassword", + "rstest", "semver", "serde", "serde_json", diff --git a/tembo-cli/Cargo.toml b/tembo-cli/Cargo.toml index 3a31f9c20..8de205c92 100644 --- a/tembo-cli/Cargo.toml +++ b/tembo-cli/Cargo.toml @@ -1,7 +1,7 @@ workspace = { members = ["temboclient", "tembodataclient"] } [package] name = "tembo-cli" -version = "0.13.4" +version = "0.13.5" edition = "2021" authors = ["Tembo.io"] description = "The CLI for Tembo" @@ -72,3 +72,4 @@ openssl = { version = "0.10", features = ["vendored"] } [dev-dependencies] assert_cmd = "2.0.8" predicates = "2.1.5" +rstest = "0.18" diff --git a/tembo-cli/README.md b/tembo-cli/README.md index b30560152..a0e15afbf 100644 --- a/tembo-cli/README.md +++ b/tembo-cli/README.md @@ -105,85 +105,90 @@ openapi-generator generate -i https://api.tembo.io/api-docs/openapi.json -g rus **TODO:** Find a better way to do this. ``` - use std::str::FromStr; -use super::{Cpu, Storage, StackType, Memory, Environment}; +use super::{Cpu, Environment, Memory, StackType, Storage}; impl FromStr for Cpu { - type Err = (); - - fn from_str(input: &str) -> core::result::Result { - match input { - "1" => Ok(Cpu::Variant1), - "2" => Ok(Cpu::Variant2), - "4" => Ok(Cpu::Variant4), - "8" => Ok(Cpu::Variant8), - "16" => Ok(Cpu::Variant16), - "32" => Ok(Cpu::Variant32), - _ => Err(()), - } - } + type Err = (); + + fn from_str(input: &str) -> core::result::Result { + match input { + "0.25" => Ok(Cpu::Variant0Period25), + "0.5" => Ok(Cpu::Variant0Period5), + "1" => Ok(Cpu::Variant1), + "2" => Ok(Cpu::Variant2), + "4" => Ok(Cpu::Variant4), + "8" => Ok(Cpu::Variant8), + "16" => Ok(Cpu::Variant16), + "32" => Ok(Cpu::Variant32), + _ => Err(()), + } + } } impl FromStr for Memory { - type Err = (); - - fn from_str(input: &str) -> core::result::Result { - match input { - "1Gi" => Ok(Memory::Variant1Gi), - "2Gi" => Ok(Memory::Variant2Gi), - "4Gi" => Ok(Memory::Variant4Gi), - "8Gi" => Ok(Memory::Variant8Gi), - "16Gi" => Ok(Memory::Variant16Gi), - "32Gi" => Ok(Memory::Variant32Gi), - _ => Err(()), - } - } + type Err = (); + + fn from_str(input: &str) -> core::result::Result { + match input { + "1Gi" => Ok(Memory::Variant1Gi), + "2Gi" => Ok(Memory::Variant2Gi), + "4Gi" => Ok(Memory::Variant4Gi), + "8Gi" => Ok(Memory::Variant8Gi), + "16Gi" => Ok(Memory::Variant16Gi), + "32Gi" => Ok(Memory::Variant32Gi), + _ => Err(()), + } + } } impl FromStr for Environment { - type Err = (); - - fn from_str(input: &str) -> core::result::Result { - match input { - "dev" => Ok(Environment::Dev), - "test" => Ok(Environment::Test), - "prod" => Ok(Environment::Prod), - _ => Err(()), - } - } + type Err = (); + + fn from_str(input: &str) -> core::result::Result { + match input { + "dev" => Ok(Environment::Dev), + "test" => Ok(Environment::Test), + "prod" => Ok(Environment::Prod), + _ => Err(()), + } + } } impl FromStr for Storage { - type Err = (); - - fn from_str(input: &str) -> core::result::Result { - match input { - "10Gi" => Ok(Storage::Variant10Gi), - "50Gi" => Ok(Storage::Variant50Gi), - "100Gi" => Ok(Storage::Variant100Gi), - "200Gi" => Ok(Storage::Variant200Gi), - "300Gi" => Ok(Storage::Variant300Gi), - "400Gi" => Ok(Storage::Variant400Gi), - "500Gi" => Ok(Self::Variant500Gi), - _ => Err(()), - } - } + type Err = (); + + fn from_str(input: &str) -> core::result::Result { + match input { + "10Gi" => Ok(Storage::Variant10Gi), + "50Gi" => Ok(Storage::Variant50Gi), + "100Gi" => Ok(Storage::Variant100Gi), + "200Gi" => Ok(Storage::Variant200Gi), + "300Gi" => Ok(Storage::Variant300Gi), + "400Gi" => Ok(Storage::Variant400Gi), + "500Gi" => Ok(Self::Variant500Gi), + _ => Err(()), + } + } } -impl ToString for StackType { - fn to_string(&self) -> String { - match self { - Self::Standard => String::from("Standard"), - Self::MessageQueue => String::from("MessageQueue"), - Self::MachineLearning => String::from("MachineLearning"), - Self::Olap => String::from("OLAP"), - Self::Oltp => String::from("OLTP"), - Self::VectorDb => String::from("VectorDB"), - Self::DataWarehouse => String::from("DataWarehouse"), - } - } +impl FromStr for StackType { + type Err = (); + + fn from_str(input: &str) -> core::result::Result { + match input { + "Standard" => Ok(StackType::Standard), + "MessageQueue" => Ok(StackType::MessageQueue), + "MachineLearning" => Ok(StackType::MachineLearning), + "OLAP" => Ok(StackType::Olap), + "VectorDB" => Ok(StackType::VectorDb), + "OLTP" => Ok(StackType::Oltp), + "DataWarehouse" => Ok(StackType::DataWarehouse), + "Geospatial" => Ok(StackType::Geospatial), + _ => Err(()), + } + } } ``` diff --git a/tembo-cli/src/cli/tembo_config.rs b/tembo-cli/src/cli/tembo_config.rs index f3aba78b4..c076db606 100644 --- a/tembo-cli/src/cli/tembo_config.rs +++ b/tembo-cli/src/cli/tembo_config.rs @@ -58,11 +58,11 @@ fn default_cpu() -> String { } fn default_memory() -> String { - "1GiB".to_string() + "1Gi".to_string() } fn default_storage() -> String { - "10GiB".to_string() + "10Gi".to_string() } fn default_replicas() -> i32 { diff --git a/tembo-cli/src/cmd/validate.rs b/tembo-cli/src/cmd/validate.rs index c6a45f070..b6db79ebb 100644 --- a/tembo-cli/src/cmd/validate.rs +++ b/tembo-cli/src/cmd/validate.rs @@ -5,7 +5,7 @@ use crate::tui::white_confirmation; use anyhow::Error; use anyhow::Ok; use clap::Args; -use std::{collections::HashMap, fs, path::Path}; +use std::{collections::HashMap, fs, path::Path, str::FromStr}; /// Validates the tembo.toml file, context file, etc. #[derive(Args)] @@ -44,7 +44,16 @@ pub fn execute(verbose: bool) -> Result<(), anyhow::Error> { file_path.push_str("/tembo.toml"); let contents = fs::read_to_string(file_path.clone())?; - let _: HashMap = toml::from_str(&contents)?; + let config: HashMap = toml::from_str(&contents)?; + + // Validate the config + match validate_config(config, verbose) { + std::result::Result::Ok(_) => (), + std::result::Result::Err(e) => { + println!("Error validating config: {}", e); + has_error = true; + } + } } if verbose { println!("- Tembo file exists"); @@ -58,3 +67,212 @@ pub fn execute(verbose: bool) -> Result<(), anyhow::Error> { Ok(()) } + +fn validate_config( + config: HashMap, + verbose: bool, +) -> Result<(), anyhow::Error> { + for (section, settings) in config { + // Validate the environment + let env_str = settings.environment.as_str(); + validate_environment(env_str, §ion, verbose)?; + + // Validate the cpu + let cpu_str = settings.cpu.as_str(); + validate_cpu(cpu_str, §ion, verbose)?; + + // Validate the memory + let memory_str = settings.memory.as_str(); + validate_memory(memory_str, §ion, verbose)?; + + // Validate the storage + let storage_str = settings.storage.as_str(); + validate_storage(storage_str, §ion, verbose)?; + + // Validate the replicas + let replicas_str = settings.replicas.to_string(); + validate_replicas(&replicas_str, §ion, verbose)?; + + // Validate the stack types + let stack_types_str = settings.stack_type.as_str(); + validate_stack_type(stack_types_str, §ion, verbose)?; + } + Ok(()) +} + +fn validate_environment(env: &str, section: &str, verbose: bool) -> Result<(), anyhow::Error> { + match temboclient::models::Environment::from_str(env) { + std::result::Result::Ok(_) => { + if verbose { + println!("- Environment '{}' in section '{}' is valid", env, section); + } + Ok(()) + } + std::result::Result::Err(_) => Err(Error::msg(format!( + "Invalid environment setting in section '{}': {}", + section, env + ))), + } +} + +fn validate_cpu(cpu: &str, section: &str, verbose: bool) -> Result<(), anyhow::Error> { + match temboclient::models::Cpu::from_str(cpu) { + std::result::Result::Ok(_) => { + if verbose { + println!("- Cpu '{}' in section '{}' is valid", cpu, section); + } + Ok(()) + } + std::result::Result::Err(_) => Err(Error::msg(format!( + "Invalid cpu setting in section '{}': {}", + section, cpu + ))), + } +} + +fn validate_memory(memory: &str, section: &str, verbose: bool) -> Result<(), anyhow::Error> { + match temboclient::models::Memory::from_str(memory) { + std::result::Result::Ok(_) => { + if verbose { + println!("- Memory '{}' in section '{}' is valid", memory, section); + } + Ok(()) + } + std::result::Result::Err(_) => Err(Error::msg(format!( + "Invalid memory setting in section '{}': {}", + section, memory + ))), + } +} + +fn validate_storage(storage: &str, section: &str, verbose: bool) -> Result<(), anyhow::Error> { + match temboclient::models::Storage::from_str(storage) { + std::result::Result::Ok(_) => { + if verbose { + println!("- Storage '{}' in section '{}' is valid", storage, section); + } + Ok(()) + } + std::result::Result::Err(_) => Err(Error::msg(format!( + "Invalid storage setting in section '{}': {}", + section, storage + ))), + } +} + +fn validate_replicas(replicas: &str, section: &str, verbose: bool) -> Result<(), anyhow::Error> { + match replicas.parse::() { + std::result::Result::Ok(value) => { + if value == 1 || value == 2 { + if verbose { + println!( + "- Replicas '{}' in section '{}' is valid", + replicas, section + ); + } + Ok(()) + } else { + Err(Error::msg(format!( + "Invalid replicas setting in section '{}': {}. Value must be 1 or 2.", + section, replicas + ))) + } + } + Err(_) => Err(Error::msg(format!( + "Invalid replicas setting in section '{}': {}. Value must be a number.", + section, replicas + ))), + } +} + +fn validate_stack_type( + stack_types: &str, + section: &str, + verbose: bool, +) -> Result<(), anyhow::Error> { + match temboclient::models::StackType::from_str(stack_types) { + std::result::Result::Ok(_) => { + if verbose { + println!( + "- Stack types '{}' in section '{}' is valid", + stack_types, section + ); + } + Ok(()) + } + std::result::Result::Err(_) => Err(Error::msg(format!( + "Invalid stack types setting in section '{}': {}", + section, stack_types + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("prod", true)] + #[case("dev", true)] + #[case("test", true)] + #[case("invalid_env", false)] + fn test_validate_environment(#[case] env: &str, #[case] is_valid: bool) { + let result = validate_environment(env, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } + + #[rstest] + #[case("0.25", true)] + #[case("0.5", true)] + #[case("0.75", false)] + #[case("1", true)] + #[case("2", true)] + #[case("4", true)] + #[case("7", false)] + fn test_validate_cpu(#[case] cpu: &str, #[case] is_valid: bool) { + let result = validate_cpu(cpu, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } + + #[rstest] + #[case("1Gi", true)] + #[case("2Gi", true)] + #[case("4Gi", true)] + #[case("16gi", false)] + fn test_validate_memory(#[case] memory: &str, #[case] is_valid: bool) { + let result = validate_memory(memory, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } + + #[rstest] + #[case("10Gi", true)] + #[case("50Gi", true)] + #[case("100Gi", true)] + #[case("120Gi", false)] + #[case("200gi", false)] + fn test_validate_storage(#[case] storage: &str, #[case] is_valid: bool) { + let result = validate_storage(storage, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } + + #[rstest] + #[case("1", true)] + #[case("2", true)] + #[case("4", false)] + fn test_validate_replicas(#[case] replicas: &str, #[case] is_valid: bool) { + let result = validate_replicas(replicas, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } + + #[rstest] + #[case("Standard", true)] + #[case("VectorDB", true)] + #[case("OLTP", true)] + #[case("OLAP", true)] + #[case("datawarehouse", false)] + fn test_validate_stack_type(#[case] stack_type: &str, #[case] is_valid: bool) { + let result = validate_stack_type(stack_type, "test_section", false); + assert_eq!(result.is_ok(), is_valid); + } +} diff --git a/tembo-cli/temboclient/src/models/impls.rs b/tembo-cli/temboclient/src/models/impls.rs index 18abbfd7d..9972919ca 100644 --- a/tembo-cli/temboclient/src/models/impls.rs +++ b/tembo-cli/temboclient/src/models/impls.rs @@ -7,6 +7,8 @@ impl FromStr for Cpu { fn from_str(input: &str) -> core::result::Result { match input { + "0.25" => Ok(Cpu::Variant0Period25), + "0.5" => Ok(Cpu::Variant0Period5), "1" => Ok(Cpu::Variant1), "2" => Ok(Cpu::Variant2), "4" => Ok(Cpu::Variant4), @@ -76,6 +78,7 @@ impl FromStr for StackType { "VectorDB" => Ok(StackType::VectorDb), "OLTP" => Ok(StackType::Oltp), "DataWarehouse" => Ok(StackType::DataWarehouse), + "Geospatial" => Ok(StackType::Geospatial), _ => Err(()), } }