From 49cd87cd2c9fbaa169cd2b153c34a19018280812 Mon Sep 17 00:00:00 2001 From: Adam Hendel Date: Tue, 7 May 2024 10:13:13 -0500 Subject: [PATCH] move Stacks objects to tembo-stacks (#764) --- tembo-stacks/Cargo.lock | 11 +- tembo-stacks/Cargo.toml | 7 +- tembo-stacks/src/stacks/config_engines.rs | 513 ++++++++++++++++++++++ tembo-stacks/src/stacks/mod.rs | 4 +- tembo-stacks/src/stacks/types.rs | 110 ++++- 5 files changed, 633 insertions(+), 12 deletions(-) create mode 100644 tembo-stacks/src/stacks/config_engines.rs diff --git a/tembo-stacks/Cargo.lock b/tembo-stacks/Cargo.lock index 7ad46fa72..4488fcbbc 100644 --- a/tembo-stacks/Cargo.lock +++ b/tembo-stacks/Cargo.lock @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "controller" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89857fc33b01e46073edf982d450a71396a5d9eb167c8878185cbca037ecbe5d" +checksum = "c2a3e5feec969cab81365dc99caf804454dfcdb58fdc58be4a610117e3cce70c" dependencies = [ "actix-web", "anyhow", @@ -1866,9 +1866,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -2298,13 +2298,14 @@ dependencies = [ [[package]] name = "tembo-stacks" -version = "0.6.0" +version = "0.7.0" dependencies = [ "anyhow", "controller", "futures", "k8s-openapi", "lazy_static", + "regex", "schemars", "serde", "serde_json", diff --git a/tembo-stacks/Cargo.toml b/tembo-stacks/Cargo.toml index eeed6c0d9..8f4599c75 100644 --- a/tembo-stacks/Cargo.toml +++ b/tembo-stacks/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tembo-stacks" description = "Tembo Stacks for Postgres" -version = "0.6.0" +version = "0.7.0" authors = ["tembo.io"] edition = "2021" license = "Apache-2.0" @@ -11,14 +11,15 @@ repository = "https://github.com/tembo-io/tembo" [dependencies] anyhow = "1.0.71" futures = "0.3.28" +k8s-openapi = { version = "0.18.0", features = ["v1_25", "schemars"], default-features = false } # This version has to be in line with the same version we use in the controller lazy_static = "1.4.0" +regex = "1.10.4" schemars = {version = "0.8.12", features = ["chrono"]} -k8s-openapi = { version = "0.18.0", features = ["v1_25", "schemars"], default-features = false } # This version has to be in line with the same version we use in the controller serde = "1.0.152" serde_yaml = "0.9.21" strum = "0.26.2" strum_macros = "0.26.2" -tembo-controller = { package = "controller", version = "0.46.0" } +tembo-controller = { package = "controller", version = "0.47.0" } tracing = "0.1" utoipa = { version = "3", features = ["actix_extras", "chrono"] } diff --git a/tembo-stacks/src/stacks/config_engines.rs b/tembo-stacks/src/stacks/config_engines.rs new file mode 100644 index 000000000..f89dbaed3 --- /dev/null +++ b/tembo-stacks/src/stacks/config_engines.rs @@ -0,0 +1,513 @@ +use anyhow::Result; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::stacks::types::Stack; +use lazy_static::lazy_static; +use regex::Regex; +use tembo_controller::{ + apis::postgres_parameters::{ConfigValue, PgConfig}, + errors::ValueError, +}; + +const DEFAULT_MAINTENANCE_WORK_MEM_MB: i32 = 64; +const DEFAULT_EFFECTIVE_IO_CONCURRENCY: i32 = 100; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ConfigEngine { + Standard, + OLAP, + MQ, +} + +// The standard configuration engine +// intended to be used as a baseline for other configuration engines +pub fn standard_config_engine(stack: &Stack) -> Vec { + let sys_mem_mb = parse_memory(stack).expect("no memory values"); + let sys_storage_gb = parse_storage(stack).expect("no storage values"); + + let shared_buffer_val_mb = standard_shared_buffers(sys_mem_mb); + let max_connections: i32 = standard_max_connections(sys_mem_mb); + let work_mem = dynamic_work_mem(sys_mem_mb as i32, shared_buffer_val_mb, max_connections); + let bgwriter_delay_ms = standard_bgwriter_delay_ms(sys_mem_mb as i32); + let effective_cache_size_mb = dynamic_effective_cache_size_mb(sys_mem_mb as i32); + let maintenance_work_mem_mb = dynamic_maintenance_work_mem_mb(sys_mem_mb as i32); + let max_wal_size_gb = dynamic_max_wal_size(sys_storage_gb as i32); + let effective_io_concurrency: i32 = DEFAULT_EFFECTIVE_IO_CONCURRENCY; + + vec![ + PgConfig { + name: "shared_buffers".to_owned(), + value: ConfigValue::Single(format!("{shared_buffer_val_mb}MB")), + }, + PgConfig { + name: "max_connections".to_owned(), + value: ConfigValue::Single(max_connections.to_string()), + }, + PgConfig { + name: "work_mem".to_owned(), + value: ConfigValue::Single(format!("{work_mem}MB")), + }, + PgConfig { + name: "bgwriter_delay".to_owned(), + value: ConfigValue::Single(format!("{bgwriter_delay_ms}ms")), + }, + PgConfig { + name: "effective_cache_size".to_owned(), + value: ConfigValue::Single(format!("{effective_cache_size_mb}MB")), + }, + PgConfig { + name: "maintenance_work_mem".to_owned(), + value: ConfigValue::Single(format!("{maintenance_work_mem_mb}MB")), + }, + PgConfig { + name: "max_wal_size".to_owned(), + value: ConfigValue::Single(format!("{max_wal_size_gb}GB")), + }, + PgConfig { + name: "effective_io_concurrency".to_owned(), + value: ConfigValue::Single(effective_io_concurrency.to_string()), + }, + ] +} + +pub fn olap_config_engine(stack: &Stack) -> Vec { + let sys_mem_mb = parse_memory(stack).expect("no memory values"); + let sys_storage_gb = parse_storage(stack).expect("no storage values"); + let vcpu = parse_cpu(stack); + + let shared_buffer_val_mb = standard_shared_buffers(sys_mem_mb); + let max_connections: i32 = olap_max_connections(sys_mem_mb as i32); + let work_mem = dynamic_work_mem(sys_mem_mb as i32, shared_buffer_val_mb, max_connections); + let effective_cache_size_mb = dynamic_effective_cache_size_mb(sys_mem_mb as i32); + let maintenance_work_mem_mb = olap_maintenance_work_mem_mb(sys_mem_mb as i32); + let max_wal_size_gb: i32 = dynamic_max_wal_size(sys_storage_gb as i32); + let max_parallel_workers = olap_max_parallel_workers(vcpu); + let max_parallel_workers_per_gather = olap_max_parallel_workers_per_gather(vcpu); + let max_worker_processes = olap_max_worker_processes(vcpu); + let effective_io_concurrency: i32 = DEFAULT_EFFECTIVE_IO_CONCURRENCY; + let columnar_min_parallel_processes = olap_max_worker_processes(vcpu); + vec![ + PgConfig { + name: "effective_cache_size".to_owned(), + value: ConfigValue::Single(format!("{effective_cache_size_mb}MB")), + }, + PgConfig { + name: "maintenance_work_mem".to_owned(), + value: ConfigValue::Single(format!("{maintenance_work_mem_mb}MB")), + }, + PgConfig { + name: "max_connections".to_owned(), + value: ConfigValue::Single(max_connections.to_string()), + }, + PgConfig { + name: "max_parallel_workers".to_owned(), + value: ConfigValue::Single(max_parallel_workers.to_string()), + }, + PgConfig { + name: "max_parallel_workers_per_gather".to_owned(), + value: ConfigValue::Single(max_parallel_workers_per_gather.to_string()), + }, + PgConfig { + name: "max_wal_size".to_owned(), + value: ConfigValue::Single(format!("{max_wal_size_gb}GB")), + }, + PgConfig { + name: "max_worker_processes".to_owned(), + value: ConfigValue::Single(max_worker_processes.to_string()), + }, + PgConfig { + name: "shared_buffers".to_owned(), + value: ConfigValue::Single(format!("{shared_buffer_val_mb}MB")), + }, + PgConfig { + name: "work_mem".to_owned(), + value: ConfigValue::Single(format!("{work_mem}MB")), + }, + PgConfig { + name: "effective_io_concurrency".to_owned(), + value: ConfigValue::Single(effective_io_concurrency.to_string()), + }, + PgConfig { + name: "columnar.min_parallel_processes".to_owned(), + value: ConfigValue::Single(columnar_min_parallel_processes.to_string()), + }, + ] +} + +// the MQ config engine is essentially the standard OLTP config engine, with a few tweaks +pub fn mq_config_engine(stack: &Stack) -> Vec { + let sys_mem_mb = parse_memory(stack).expect("no memory values"); + let shared_buffer_val_mb = mq_shared_buffers(sys_mem_mb); + + // start with the output from the standard config engine + let mut configs = standard_config_engine(stack); + + for config in configs.iter_mut() { + if config.name == "shared_buffers" { + config.value = ConfigValue::Single(format!("{shared_buffer_val_mb}MB")) + } + } + + configs +} + +// olap formula for max_parallel_workers_per_gather +fn olap_max_parallel_workers_per_gather(cpu: f32) -> i32 { + // higher of default (2) or 0.5 * cpu + let scaled = i32::max((cpu * 0.5).floor() as i32, 2); + // cap at 8 + i32::max(scaled, 8) +} + +fn olap_max_parallel_workers(cpu: f32) -> i32 { + // higher of the default (8) or cpu + i32::max(8, cpu.round() as i32) +} + +fn olap_max_worker_processes(cpu: f32) -> i32 { + i32::max(1, cpu.round() as i32) +} + +// olap formula for maintenance_work_mem +fn olap_maintenance_work_mem_mb(sys_mem_mb: i32) -> i32 { + // max of the default 64MB and 10% of system memory + const MAINTENANCE_WORK_MEM_RATIO: f64 = 0.10; + i32::max( + DEFAULT_MAINTENANCE_WORK_MEM_MB, + (sys_mem_mb as f64 * MAINTENANCE_WORK_MEM_RATIO).floor() as i32, + ) +} + +// general purpose formula for maintenance_work_mem +fn dynamic_maintenance_work_mem_mb(sys_mem_mb: i32) -> i32 { + // max of the default 64MB and 5% of system memory + const MAINTENANCE_WORK_MEM_RATIO: f64 = 0.05; + const DEFAULT_MAINTENANCE_WORK_MEM_MB: i32 = 64; + i32::max( + DEFAULT_MAINTENANCE_WORK_MEM_MB, + (sys_mem_mb as f64 * MAINTENANCE_WORK_MEM_RATIO).floor() as i32, + ) +} + +fn dynamic_max_wal_size(sys_disk_gb: i32) -> i32 { + // maximum percentage of disk to give to the WAL process + // TODO: ideal should be: min(20% of disk, f(disk throughput)) + // also, this will panic if < 10GB disk, which is not supported + if sys_disk_gb < 10 { + panic!("disk size must be greater than 10GB") + } else if sys_disk_gb <= 100 { + (sys_disk_gb as f32 * 0.2).floor() as i32 + } else if sys_disk_gb <= 1000 { + (sys_disk_gb as f32 * 0.1).floor() as i32 + } else { + (sys_disk_gb as f32 * 0.05).floor() as i32 + } +} + +// piecewise function to set the background writer delay +fn standard_bgwriter_delay_ms(sys_mem_mb: i32) -> i32 { + // bgwriter_delay = ≥ 8Gb ram - set to 10, set to 200 for everything smaller than 8Gb + if sys_mem_mb >= 8192 { + 10 + } else { + 200 + } +} + +// in olap, we want to limit the number of connections +// never to exceed MAX_CONNECTIONS +fn olap_max_connections(sys_mem_mb: i32) -> i32 { + const MAX_CONNECTIONS: i32 = 100; + i32::min(standard_max_connections(sys_mem_mb as f64), MAX_CONNECTIONS) +} + +// returns Memory from a Stack in Mb +fn parse_memory(stack: &Stack) -> Result { + let mem_str = stack + .infrastructure + .as_ref() + .expect("infra required for a configuration engine") + .memory + .clone(); + let (mem, unit) = split_string(&mem_str)?; + match unit.as_str() { + "Gi" => Ok(mem * 1024.0), + "Mi" => Ok(mem), + _ => Err(ValueError::Invalid(format!( + "Invalid mem value: {}", + mem_str + ))), + } +} + +// returns the Storage from a Stack in GB +fn parse_storage(stack: &Stack) -> Result { + let storage_str = stack + .infrastructure + .as_ref() + .expect("infra required for a configuration engine") + .storage + .clone(); + let (storage, unit) = split_string(&storage_str)?; + match unit.as_str() { + "Gi" => Ok(storage), + _ => Err(ValueError::Invalid(format!( + "Invalid storage value: {}", + storage_str + ))), + } +} + +// Standard formula for shared buffers, 25% of system memory +// returns the value as string including units, e.g. 128MB +fn mq_shared_buffers(mem_mb: f64) -> i32 { + (mem_mb * 0.6).floor() as i32 +} + +// Standard formula for shared buffers, 25% of system memory +// returns the value as string including units, e.g. 128MB +fn standard_shared_buffers(mem_mb: f64) -> i32 { + (mem_mb / 4.0_f64).floor() as i32 +} + +fn standard_max_connections(mem_mb: f64) -> i32 { + const MEM_PER_CONNECTION_MB: f64 = 9.5; + (mem_mb / MEM_PER_CONNECTION_MB).floor() as i32 +} + +// returns work_mem value in MB +fn dynamic_work_mem(sys_mem_mb: i32, shared_buffers_mb: i32, max_connections: i32) -> i32 { + (((sys_mem_mb - shared_buffers_mb) as f64 - (sys_mem_mb as f64 * 0.2)) / max_connections as f64) + .floor() as i32 +} + +// generally safe for most workloads +fn dynamic_effective_cache_size_mb(sys_mem_mb: i32) -> i32 { + const EFFECTIVE_CACHE_SIZE: f64 = 0.70; + (sys_mem_mb as f64 * EFFECTIVE_CACHE_SIZE).floor() as i32 +} + +lazy_static! { + static ref RE: Regex = Regex::new(r"^([0-9]*\.?[0-9]+)([a-zA-Z]+)$").unwrap(); +} + +fn split_string(input: &str) -> Result<(f64, String), ValueError> { + if let Some(cap) = RE.captures(input) { + let num = cap[1].parse::()?; + let alpha = cap[2].to_string(); + Ok((num, alpha)) + } else { + Err(ValueError::Invalid(format!( + "Invalid string format: {}", + input + ))) + } +} + +// returns the vCPU count +fn parse_cpu(stack: &Stack) -> f32 { + stack + .infrastructure + .as_ref() + .expect("infra required for a configuration engine") + .cpu + .to_string() + .parse::() + .expect("failed parsing cpu") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::stacks::types::*; + + #[test] + #[should_panic] + fn test_invalid_storage_dynamic_wal() { + dynamic_max_wal_size(9); + } + + #[test] + fn test_dynamic_max_wall_size() { + let max_wal_size = dynamic_max_wal_size(100); + assert_eq!(max_wal_size, 20); + let max_wal_size = dynamic_max_wal_size(1000); + assert_eq!(max_wal_size, 100); + let max_wal_size = dynamic_max_wal_size(3000); + assert_eq!(max_wal_size, 150); + } + + #[test] + fn test_dynamic_maintenance_work_mem_mb() { + let work_mem = dynamic_maintenance_work_mem_mb(1024); + assert_eq!(work_mem, 64); + let work_mem = dynamic_maintenance_work_mem_mb(10240); + assert_eq!(work_mem, 512); + } + + #[test] + fn test_effective_cache_size() { + let work_mem = dynamic_effective_cache_size_mb(1024); + assert_eq!(work_mem, 716); + } + + #[test] + fn test_standard_bgwriter_delay_ms() { + let bgwriter_delay = standard_bgwriter_delay_ms(1024); + assert_eq!(bgwriter_delay, 200); + let bgwriter_delay = standard_bgwriter_delay_ms(8192); + assert_eq!(bgwriter_delay, 10); + } + + #[test] + fn test_dynamic_work_mem() { + let work_mem = dynamic_work_mem(1024, 250, 107); + assert_eq!(work_mem, 5); + + let work_mem = dynamic_work_mem(16384, 4096, 100); + assert_eq!(work_mem, 90); + } + + #[test] + fn test_standard_config_engine() { + let mut stack = Stack { + name: "test".to_owned(), + postgres_config_engine: Some(ConfigEngine::Standard), + ..Stack::default() + }; + let infra = Infrastructure { + cpu: "1".to_string(), + memory: "16Gi".to_string(), + storage: "10Gi".to_string(), + }; + stack.infrastructure = Some(infra); + let configs = standard_config_engine(&stack); + assert_eq!(configs[0].name, "shared_buffers"); + assert_eq!(configs[0].value.to_string(), "4096MB"); + assert_eq!(configs[1].name, "max_connections"); + assert_eq!(configs[1].value.to_string(), "1724"); + assert_eq!(configs[2].name, "work_mem"); + assert_eq!(configs[2].value.to_string(), "5MB"); + assert_eq!(configs[3].name, "bgwriter_delay"); + assert_eq!(configs[3].value.to_string(), "10ms"); + assert_eq!(configs[4].name, "effective_cache_size"); + assert_eq!(configs[4].value.to_string(), "11468MB"); + assert_eq!(configs[5].name, "maintenance_work_mem"); + assert_eq!(configs[5].value.to_string(), "819MB"); + assert_eq!(configs[6].name, "max_wal_size"); + assert_eq!(configs[6].value.to_string(), "2GB"); + } + + #[test] + fn test_standard_shared_buffers() { + let shared_buff = standard_shared_buffers(1024.0); + assert_eq!(shared_buff, 256); + let shared_buff = standard_shared_buffers(10240.0); + assert_eq!(shared_buff, 2560); + + let shared_buff = mq_shared_buffers(1024.0); + assert_eq!(shared_buff, 614); + + let shared_buff = mq_shared_buffers(10240.0); + assert_eq!(shared_buff, 6144); + } + + #[test] + fn test_olap_max_connections() { + // capped at 100 + let max_con = olap_max_connections(4096); + assert_eq!(max_con, 100); + let max_con = olap_max_connections(1024); + assert_eq!(max_con, 100); + } + + #[test] + fn test_standard_max_connections() { + let max_connections = standard_max_connections(1024.0); + assert_eq!(max_connections, 107); + let max_connections = standard_max_connections(8192.0); + assert_eq!(max_connections, 862); + } + + #[test] + fn test_split_string() { + let (mem, unit) = split_string("10Gi").expect("failed parsing val"); + assert_eq!(mem, 10.0); + assert_eq!(unit, "Gi"); + + let error_val = split_string("BadData"); + assert!(error_val.is_err()); + let error_val: Result<(f64, String), ValueError> = split_string("Gi10"); + assert!(error_val.is_err()); + } + + #[test] + fn test_olap_config_engine() { + let stack = Stack { + name: "test".to_owned(), + infrastructure: Some(Infrastructure { + cpu: "4".to_string(), + memory: "16Gi".to_string(), + storage: "10Gi".to_string(), + }), + postgres_config_engine: Some(ConfigEngine::Standard), + ..Stack::default() + }; + let configs = olap_config_engine(&stack); + + assert_eq!(configs[0].name, "effective_cache_size"); + assert_eq!(configs[0].value.to_string(), "11468MB"); + assert_eq!(configs[1].name, "maintenance_work_mem"); + assert_eq!(configs[1].value.to_string(), "1638MB"); + assert_eq!(configs[2].name, "max_connections"); + assert_eq!(configs[2].value.to_string(), "100"); + assert_eq!(configs[3].name, "max_parallel_workers"); + assert_eq!(configs[3].value.to_string(), "8"); + assert_eq!(configs[4].name, "max_parallel_workers_per_gather"); + assert_eq!(configs[4].value.to_string(), "8"); + assert_eq!(configs[5].name, "max_wal_size"); + assert_eq!(configs[5].value.to_string(), "2GB"); + assert_eq!(configs[6].name, "max_worker_processes"); + assert_eq!(configs[6].value.to_string(), "4"); + assert_eq!(configs[7].name, "shared_buffers"); + assert_eq!(configs[7].value.to_string(), "4096MB"); + assert_eq!(configs[8].name, "work_mem"); + assert_eq!(configs[8].value.to_string(), "90MB"); + } + + #[test] + fn test_olap_config_engine_fractional_cpu() { + let stack = Stack { + name: "test".to_owned(), + infrastructure: Some(Infrastructure { + cpu: "0.5".to_string(), + memory: "8Gi".to_string(), + storage: "10Gi".to_string(), + }), + postgres_config_engine: Some(ConfigEngine::Standard), + ..Stack::default() + }; + let configs = olap_config_engine(&stack); + assert_eq!(configs[0].name, "effective_cache_size"); + assert_eq!(configs[0].value.to_string(), "5734MB"); + assert_eq!(configs[1].name, "maintenance_work_mem"); + assert_eq!(configs[1].value.to_string(), "819MB"); + assert_eq!(configs[2].name, "max_connections"); + assert_eq!(configs[2].value.to_string(), "100"); + assert_eq!(configs[3].name, "max_parallel_workers"); + assert_eq!(configs[3].value.to_string(), "8"); + assert_eq!(configs[4].name, "max_parallel_workers_per_gather"); + assert_eq!(configs[4].value.to_string(), "8"); + assert_eq!(configs[5].name, "max_wal_size"); + assert_eq!(configs[5].value.to_string(), "2GB"); + assert_eq!(configs[6].name, "max_worker_processes"); + assert_eq!(configs[6].value.to_string(), "1"); + assert_eq!(configs[7].name, "shared_buffers"); + assert_eq!(configs[7].value.to_string(), "2048MB"); + assert_eq!(configs[8].name, "work_mem"); + assert_eq!(configs[8].value.to_string(), "45MB"); + } +} diff --git a/tembo-stacks/src/stacks/mod.rs b/tembo-stacks/src/stacks/mod.rs index 3892a6bb9..648cc7bf9 100644 --- a/tembo-stacks/src/stacks/mod.rs +++ b/tembo-stacks/src/stacks/mod.rs @@ -1,7 +1,7 @@ +pub mod config_engines; pub mod types; -use crate::stacks::types::StackType; -use tembo_controller::stacks::types::Stack; +use crate::stacks::types::{Stack, StackType}; use lazy_static::lazy_static; diff --git a/tembo-stacks/src/stacks/types.rs b/tembo-stacks/src/stacks/types.rs index 5b9305d93..b9d81441f 100644 --- a/tembo-stacks/src/stacks/types.rs +++ b/tembo-stacks/src/stacks/types.rs @@ -1,5 +1,16 @@ +use crate::stacks::config_engines::{ + mq_config_engine, olap_config_engine, standard_config_engine, ConfigEngine, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use tembo_controller::{ + apis::postgres_parameters::PgConfig, + app_service::types::AppService, + defaults::ImagePerPgVersion, + defaults::{default_images, default_repository}, + extensions::types::{Extension, TrunkInstall}, + postgres_exporter::QueryConfig, +}; use utoipa::ToSchema; #[derive( @@ -71,12 +82,107 @@ impl StackType { } } +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, ToSchema)] +pub struct Stack { + pub name: String, + /// specifies any resource constraints that should be applied to an instance of the Stack + pub compute_constraints: Option, + pub description: Option, + /// Organization hosting the Docker images used in this stack + /// Default: "tembo" + #[serde(default = "default_organization")] + pub organization: String, + #[serde(default = "default_stack_repository")] + pub repository: String, + /// The Docker images to use for each supported Postgres versions + /// + /// Default: + /// 14: "standard-cnpg:14-a0a5ab5" + /// 15: "standard-cnpg:15-a0a5ab5" + /// 16: "standard-cnpg:16-a0a5ab5" + #[serde(default = "default_images")] + pub images: ImagePerPgVersion, + pub stack_version: Option, + pub trunk_installs: Option>, + pub extensions: Option>, + /// Postgres metric definition specific to the Stack + pub postgres_metrics: Option, + /// configs are strongly typed so that they can be programmatically transformed + pub postgres_config: Option>, + #[serde(default = "default_config_engine")] + pub postgres_config_engine: Option, + /// external application services + pub infrastructure: Option, + #[serde(rename = "appServices")] + pub app_services: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)] +pub struct ComputeConstraint { + pub min: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)] +pub struct ComputeResource { + pub cpu: Option, + pub memory: Option, +} + +impl Stack { + // https://www.postgresql.org/docs/current/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY + pub fn runtime_config(&self) -> Option> { + match &self.postgres_config_engine { + Some(ConfigEngine::Standard) => Some(standard_config_engine(self)), + Some(ConfigEngine::OLAP) => Some(olap_config_engine(self)), + Some(ConfigEngine::MQ) => Some(mq_config_engine(self)), + None => Some(standard_config_engine(self)), + } + } +} + +fn default_organization() -> String { + "tembo".into() +} + +fn default_stack_repository() -> String { + default_repository() +} + +fn default_config_engine() -> Option { + Some(ConfigEngine::Standard) +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, ToSchema)] +pub struct Infrastructure { + // generic specs + #[serde(default = "default_cpu")] + pub cpu: String, + #[serde(default = "default_memory")] + pub memory: String, + #[serde(default = "default_storage")] + pub storage: String, +} + +fn default_cpu() -> String { + "1".to_owned() +} + +fn default_memory() -> String { + "1Gi".to_owned() +} + +fn default_storage() -> String { + "10Gi".to_owned() +} + #[cfg(test)] mod tests { - use crate::stacks::{get_stack, types::StackType}; + use crate::stacks::{ + get_stack, + types::{Infrastructure, StackType}, + }; use strum::IntoEnumIterator; use tembo_controller::apis::postgres_parameters::PgConfig; - use tembo_controller::stacks::types::Infrastructure; #[test] fn test_stacks_definitions() {