diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index 365fe3f19e..884f80f15e 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -17,6 +17,7 @@ use crate::internal_api::params::DnsConfigParams; use crate::inventory::Collection; pub use crate::inventory::SourceNatConfig; pub use crate::inventory::ZpoolName; +use blueprint_diff::ClickhouseClusterConfigDiffTables; use derive_more::From; use nexus_sled_agent_shared::inventory::OmicronZoneConfig; use nexus_sled_agent_shared::inventory::OmicronZoneType; @@ -268,8 +269,7 @@ impl Blueprint { self.blueprint_zones.keys().copied() } - /// Summarize the difference between sleds and zones between two - /// blueprints. + /// Summarize the difference between two blueprints. /// /// The argument provided is the "before" side, and `self` is the "after" /// side. This matches the order of arguments to @@ -277,6 +277,7 @@ impl Blueprint { pub fn diff_since_blueprint(&self, before: &Blueprint) -> BlueprintDiff { BlueprintDiff::new( DiffBeforeMetadata::Blueprint(Box::new(before.metadata())), + DiffBeforeClickhouseClusterConfig::from(before), before.sled_state.clone(), before .blueprint_zones @@ -288,15 +289,11 @@ impl Blueprint { .iter() .map(|(sled_id, disks)| (*sled_id, disks.clone().into())) .collect(), - self.metadata(), - self.sled_state.clone(), - self.blueprint_zones.clone(), - self.blueprint_disks.clone(), + &self, ) } - /// Summarize the differences in sleds and zones between a collection and a - /// blueprint. + /// Summarize the differences between a collection and a blueprint. /// /// This gives an idea about what would change about a running system if /// one were to execute the blueprint. @@ -339,13 +336,11 @@ impl Blueprint { BlueprintDiff::new( DiffBeforeMetadata::Collection { id: before.id }, + DiffBeforeClickhouseClusterConfig::from(before), before_state, before_zones, before_disks, - self.metadata(), - self.sled_state.clone(), - self.blueprint_zones.clone(), - self.blueprint_disks.clone(), + &self, ) } @@ -400,6 +395,30 @@ impl BpSledSubtableData for BlueprintOrCollectionZonesConfig { } } +// Useful implementation for printing two column tables stored in a `BTreeMap`, where +// each key and value impl `Display`. +impl BpSledSubtableData for (Generation, &BTreeMap) +where + S1: fmt::Display, + S2: fmt::Display, +{ + fn bp_generation(&self) -> BpGeneration { + BpGeneration::Value(self.0) + } + + fn rows( + &self, + state: BpDiffState, + ) -> impl Iterator { + self.1.iter().map(move |(s1, s2)| { + BpSledSubtableRow::from_strings( + state, + vec![s1.to_string(), s2.to_string()], + ) + }) + } +} + /// Wrapper to allow a [`Blueprint`] to be displayed with information. /// /// Returned by [`Blueprint::display()`]. @@ -462,6 +481,30 @@ impl<'a> BlueprintDisplay<'a> { ], ) } + + // Return tables representing a [`ClickhouseClusterConfig`] in a given blueprint + fn make_clickhouse_cluster_config_tables( + &self, + ) -> Option<(KvListWithHeading, BpSledSubtable, BpSledSubtable)> { + let Some(config) = &self.blueprint.clickhouse_cluster_config else { + return None; + }; + + let diff_table = + ClickhouseClusterConfigDiffTables::single_blueprint_table( + BpDiffState::Unchanged, + config, + ); + + Some(( + diff_table.metadata, + diff_table.keepers, + // Safety: The call to + // `ClickhouseClusterConfigDiffTables::single_blueprint_table` + // always returns all tables. + diff_table.servers.unwrap(), + )) + } } impl<'a> fmt::Display for BlueprintDisplay<'a> { @@ -545,6 +588,11 @@ impl<'a> fmt::Display for BlueprintDisplay<'a> { } } + if let Some((t1, t2, t3)) = self.make_clickhouse_cluster_config_tables() + { + writeln!(f, "{t1}\n{t2}\n{t3}\n")?; + } + writeln!(f, "{}", self.make_cockroachdb_table())?; writeln!(f, "{}", self.make_metadata_table())?; @@ -1012,6 +1060,37 @@ impl DiffBeforeMetadata { } } +/// Data about the "before" version within a [`BlueprintDiff`] +/// +/// We only track keepers in inventory collections. +#[derive(Clone, Debug)] +pub enum DiffBeforeClickhouseClusterConfig { + Collection { + id: CollectionUuid, + latest_keeper_membership: + Option, + }, + Blueprint(Option), +} + +impl From<&Collection> for DiffBeforeClickhouseClusterConfig { + fn from(value: &Collection) -> Self { + DiffBeforeClickhouseClusterConfig::Collection { + id: value.id, + latest_keeper_membership: value + .latest_clickhouse_keeper_membership(), + } + } +} + +impl From<&Blueprint> for DiffBeforeClickhouseClusterConfig { + fn from(value: &Blueprint) -> Self { + DiffBeforeClickhouseClusterConfig::Blueprint( + value.clickhouse_cluster_config.clone(), + ) + } +} + /// Single sled's zones config for "before" version within a [`BlueprintDiff`]. #[derive(Clone, Debug)] pub enum BlueprintOrCollectionZonesConfig { diff --git a/nexus/types/src/deployment/blueprint_diff.rs b/nexus/types/src/deployment/blueprint_diff.rs index a49cbf24f1..713699e5c1 100644 --- a/nexus/types/src/deployment/blueprint_diff.rs +++ b/nexus/types/src/deployment/blueprint_diff.rs @@ -5,12 +5,16 @@ //! Types helpful for diffing blueprints. use super::blueprint_display::{ - constants::*, linear_table_modified, linear_table_unchanged, BpDiffState, - BpGeneration, BpOmicronZonesSubtableSchema, BpPhysicalDisksSubtableSchema, + constants::*, linear_table_modified, linear_table_unchanged, + BpClickhouseServersSubtableSchema, BpDiffState, BpGeneration, + BpOmicronZonesSubtableSchema, BpPhysicalDisksSubtableSchema, BpSledSubtable, BpSledSubtableColumn, BpSledSubtableData, BpSledSubtableRow, KvListWithHeading, KvPair, }; -use super::{zone_sort_key, CockroachDbPreserveDowngrade}; +use super::{ + zone_sort_key, Blueprint, ClickhouseClusterConfig, + CockroachDbPreserveDowngrade, DiffBeforeClickhouseClusterConfig, +}; use nexus_sled_agent_shared::inventory::ZoneKind; use omicron_common::api::external::Generation; use omicron_common::disk::DiskIdentity; @@ -19,6 +23,7 @@ use omicron_uuid_kinds::SledUuid; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; +use crate::deployment::blueprint_display::BpClickhouseKeepersSubtableSchema; use crate::deployment::{ BlueprintMetadata, BlueprintOrCollectionDisksConfig, BlueprintOrCollectionZoneConfig, BlueprintOrCollectionZonesConfig, @@ -566,22 +571,25 @@ pub struct BlueprintDiff { pub sleds_removed: BTreeSet, pub sleds_unchanged: BTreeSet, pub sleds_modified: BTreeSet, + pub before_clickhouse_cluster_config: DiffBeforeClickhouseClusterConfig, + pub after_clickhouse_cluster_config: Option, } impl BlueprintDiff { /// Build a diff with the provided contents, verifying that the provided /// data is valid. - #[allow(clippy::too_many_arguments)] pub fn new( before_meta: DiffBeforeMetadata, + before_clickhouse_cluster_config: DiffBeforeClickhouseClusterConfig, before_state: BTreeMap, before_zones: BTreeMap, before_disks: BTreeMap, - after_meta: BlueprintMetadata, - mut after_state: BTreeMap, - after_zones: BTreeMap, - after_disks: BTreeMap, + after_blueprint: &Blueprint, ) -> Self { + let mut after_state = after_blueprint.sled_state.clone(); + let after_zones = after_blueprint.blueprint_zones.clone(); + let after_disks = after_blueprint.blueprint_disks.clone(); + // Work around a quirk of sled decommissioning. If a sled has a before // state of `decommissioned`, it may or may not be present in // `after_state` (presence will depend on whether or not the sled was @@ -669,7 +677,7 @@ impl BlueprintDiff { BlueprintDiff { before_meta, - after_meta, + after_meta: after_blueprint.metadata(), before_state, after_state, zones, @@ -678,6 +686,10 @@ impl BlueprintDiff { sleds_removed, sleds_unchanged: unchanged_sleds, sleds_modified, + before_clickhouse_cluster_config, + after_clickhouse_cluster_config: after_blueprint + .clickhouse_cluster_config + .clone(), } } @@ -687,6 +699,368 @@ impl BlueprintDiff { } } +pub struct ClickhouseClusterConfigDiffTables { + pub metadata: KvListWithHeading, + pub keepers: BpSledSubtable, + pub servers: Option, +} + +impl ClickhouseClusterConfigDiffTables { + pub fn diff_collection_and_blueprint( + before: &clickhouse_admin_types::ClickhouseKeeperClusterMembership, + after: &ClickhouseClusterConfig, + ) -> Self { + let leader_committed_log_index = if before.leader_committed_log_index + == after.highest_seen_keeper_leader_committed_log_index + { + KvPair::new( + BpDiffState::Unchanged, + CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX, + linear_table_unchanged( + &after.highest_seen_keeper_leader_committed_log_index, + ), + ) + } else { + KvPair::new( + BpDiffState::Modified, + CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX, + linear_table_modified( + &before.leader_committed_log_index, + &after.highest_seen_keeper_leader_committed_log_index, + ), + ) + }; + let metadata = KvListWithHeading::new( + CLICKHOUSE_CLUSTER_CONFIG_HEADING, + vec![ + KvPair::new( + BpDiffState::Added, + GENERATION, + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &after.generation, + ), + ), + KvPair::new( + BpDiffState::Added, + CLICKHOUSE_MAX_USED_SERVER_ID, + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &after.max_used_server_id, + ), + ), + KvPair::new( + BpDiffState::Added, + CLICKHOUSE_MAX_USED_KEEPER_ID, + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &after.max_used_keeper_id, + ), + ), + KvPair::new( + BpDiffState::Added, + CLICKHOUSE_CLUSTER_NAME, + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &after.cluster_name, + ), + ), + KvPair::new( + BpDiffState::Added, + CLICKHOUSE_CLUSTER_SECRET, + linear_table_modified( + &NOT_PRESENT_IN_COLLECTION_PARENS, + &after.cluster_secret, + ), + ), + leader_committed_log_index, + ], + ); + + // Build up our keeper table + let mut keeper_rows = vec![]; + for (zone_id, keeper_id) in &after.keepers { + if before.raft_config.contains(keeper_id) { + // Unchanged keepers + keeper_rows.push(BpSledSubtableRow::new( + BpDiffState::Unchanged, + vec![ + BpSledSubtableColumn::Value(zone_id.to_string()), + BpSledSubtableColumn::Value(keeper_id.to_string()), + ], + )); + } else { + // Added keepers + keeper_rows.push(BpSledSubtableRow::new( + BpDiffState::Added, + vec![ + BpSledSubtableColumn::Value(zone_id.to_string()), + BpSledSubtableColumn::Value(keeper_id.to_string()), + ], + )); + } + } + + let after_ids: BTreeSet<_> = after.keepers.values().clone().collect(); + for keeper_id in &before.raft_config { + if !after_ids.contains(keeper_id) { + // Removed keepers + keeper_rows.push(BpSledSubtableRow::new( + BpDiffState::Removed, + vec![ + BpSledSubtableColumn::Value( + NOT_PRESENT_IN_COLLECTION_PARENS.to_string(), + ), + BpSledSubtableColumn::Value(keeper_id.to_string()), + ], + )); + } + } + + let keepers = BpSledSubtable::new( + BpClickhouseKeepersSubtableSchema {}, + BpGeneration::Diff { before: None, after: Some(after.generation) }, + keeper_rows, + ); + + // Build up our server table + let server_rows: Vec = after + .servers + .iter() + .map(|(zone_id, server_id)| { + BpSledSubtableRow::new( + BpDiffState::Added, + vec![ + BpSledSubtableColumn::diff( + NOT_PRESENT_IN_COLLECTION_PARENS.to_string(), + zone_id.to_string(), + ), + BpSledSubtableColumn::diff( + NOT_PRESENT_IN_COLLECTION_PARENS.to_string(), + server_id.to_string(), + ), + ], + ) + }) + .collect(); + + let servers = Some(BpSledSubtable::new( + BpClickhouseServersSubtableSchema {}, + BpGeneration::Diff { before: None, after: Some(after.generation) }, + server_rows, + )); + + ClickhouseClusterConfigDiffTables { metadata, keepers, servers } + } + + pub fn diff_blueprints( + before: &ClickhouseClusterConfig, + after: &ClickhouseClusterConfig, + ) -> Self { + macro_rules! diff_row { + ($member:ident, $label:expr) => { + if before.$member == after.$member { + KvPair::new( + BpDiffState::Unchanged, + $label, + linear_table_unchanged(&after.$member), + ) + } else { + KvPair::new( + BpDiffState::Modified, + $label, + linear_table_modified(&before.$member, &after.$member), + ) + } + }; + } + + let metadata = KvListWithHeading::new( + CLICKHOUSE_CLUSTER_CONFIG_HEADING, + vec![ + diff_row!(generation, GENERATION), + diff_row!(max_used_server_id, CLICKHOUSE_MAX_USED_SERVER_ID), + diff_row!(max_used_keeper_id, CLICKHOUSE_MAX_USED_KEEPER_ID), + diff_row!(cluster_name, CLICKHOUSE_CLUSTER_NAME), + diff_row!(cluster_secret, CLICKHOUSE_CLUSTER_SECRET), + diff_row!( + highest_seen_keeper_leader_committed_log_index, + CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX + ), + ], + ); + + // Macro used to construct keeper and server tables + macro_rules! diff_table_rows { + ($rows:ident, $collection:ident) => { + for (zone_id, id) in &after.$collection { + if before.$collection.contains_key(zone_id) { + // Unchanged + $rows.push(BpSledSubtableRow::new( + BpDiffState::Unchanged, + vec![ + BpSledSubtableColumn::Value( + zone_id.to_string(), + ), + BpSledSubtableColumn::Value(id.to_string()), + ], + )); + } else { + // Added + $rows.push(BpSledSubtableRow::new( + BpDiffState::Added, + vec![ + BpSledSubtableColumn::Value( + zone_id.to_string(), + ), + BpSledSubtableColumn::Value(id.to_string()), + ], + )); + } + } + + for (zone_id, id) in &before.$collection { + if !after.$collection.contains_key(zone_id) { + // Removed + $rows.push(BpSledSubtableRow::new( + BpDiffState::Removed, + vec![ + BpSledSubtableColumn::Value( + zone_id.to_string(), + ), + BpSledSubtableColumn::Value(id.to_string()), + ], + )); + } + } + }; + } + + // Construct our keeper table + let mut keeper_rows = vec![]; + diff_table_rows!(keeper_rows, keepers); + let keepers = BpSledSubtable::new( + BpClickhouseKeepersSubtableSchema {}, + BpGeneration::Diff { + before: Some(before.generation), + after: Some(after.generation), + }, + keeper_rows, + ); + + // Construct our server table + let mut server_rows = vec![]; + diff_table_rows!(server_rows, servers); + + let servers = Some(BpSledSubtable::new( + BpClickhouseServersSubtableSchema {}, + BpGeneration::Diff { + before: Some(before.generation), + after: Some(after.generation), + }, + server_rows, + )); + + ClickhouseClusterConfigDiffTables { metadata, keepers, servers } + } + + /// We are diffing a `Collection` and `Blueprint` but the latest blueprint + /// does not have a ClickhouseClusterConfig. + pub fn removed_from_collection( + before: &clickhouse_admin_types::ClickhouseKeeperClusterMembership, + ) -> Self { + // There's only so much information in a collection. Show what we can. + let metadata = KvListWithHeading::new( + CLICKHOUSE_CLUSTER_CONFIG_HEADING, + vec![KvPair::new( + BpDiffState::Removed, + CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX, + before.leader_committed_log_index.to_string(), + )], + ); + + let keeper_rows: Vec = before + .raft_config + .iter() + .map(|keeper_id| { + BpSledSubtableRow::new( + BpDiffState::Removed, + vec![ + BpSledSubtableColumn::Value( + NOT_PRESENT_IN_COLLECTION_PARENS.to_string(), + ), + BpSledSubtableColumn::Value(keeper_id.to_string()), + ], + ) + }) + .collect(); + + let keepers = BpSledSubtable::new( + BpClickhouseKeepersSubtableSchema {}, + BpGeneration::unknown(), + keeper_rows, + ); + + ClickhouseClusterConfigDiffTables { metadata, keepers, servers: None } + } + + /// The "before" inventory collection or blueprint does not have a relevant + /// keeper configuration. + pub fn added_to_blueprint(after: &ClickhouseClusterConfig) -> Self { + Self::single_blueprint_table(BpDiffState::Added, after) + } + + /// We are diffing two `Blueprint`s, but The latest bluerprint does not have + /// a `ClickhouseClusterConfig`. + pub fn removed_from_blueprint(before: &ClickhouseClusterConfig) -> Self { + Self::single_blueprint_table(BpDiffState::Removed, before) + } + + pub fn single_blueprint_table( + diff_state: BpDiffState, + config: &ClickhouseClusterConfig, + ) -> Self { + let rows: Vec<_> = [ + (GENERATION, config.generation.to_string()), + ( + CLICKHOUSE_MAX_USED_SERVER_ID, + config.max_used_server_id.to_string(), + ), + ( + CLICKHOUSE_MAX_USED_KEEPER_ID, + config.max_used_keeper_id.to_string(), + ), + (CLICKHOUSE_CLUSTER_NAME, config.cluster_name.clone()), + (CLICKHOUSE_CLUSTER_SECRET, config.cluster_secret.clone()), + ( + CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX, + config + .highest_seen_keeper_leader_committed_log_index + .to_string(), + ), + ] + .into_iter() + .map(|(key, val)| KvPair::new(diff_state, key, val)) + .collect(); + + let metadata = + KvListWithHeading::new(CLICKHOUSE_CLUSTER_CONFIG_HEADING, rows); + + let keepers = BpSledSubtable::new( + BpClickhouseKeepersSubtableSchema {}, + BpGeneration::Value(config.generation), + (config.generation, &config.keepers).rows(diff_state).collect(), + ); + let servers = Some(BpSledSubtable::new( + BpClickhouseServersSubtableSchema {}, + BpGeneration::Value(config.generation), + (config.generation, &config.servers).rows(diff_state).collect(), + )); + + ClickhouseClusterConfigDiffTables { metadata, keepers, servers } + } +} + /// Wrapper to allow a [`BlueprintDiff`] to be displayed. /// /// Returned by [`BlueprintDiff::display()`]. @@ -774,6 +1148,72 @@ impl<'diff> BlueprintDiffDisplay<'diff> { ] } + pub fn make_clickhouse_cluster_config_diff_tables( + &self, + ) -> Option { + match ( + &self.diff.before_clickhouse_cluster_config, + &self.diff.after_clickhouse_cluster_config, + ) { + // Before collection + after blueprint + ( + DiffBeforeClickhouseClusterConfig::Collection { + latest_keeper_membership: Some(before), + .. + }, + Some(after), + ) => Some(ClickhouseClusterConfigDiffTables::diff_collection_and_blueprint(before, after)), + + // Before collection only + ( + DiffBeforeClickhouseClusterConfig::Collection { + latest_keeper_membership: Some(before), + .. + }, + None, + ) => Some(ClickhouseClusterConfigDiffTables::removed_from_collection(before)), + + // After blueprint only + ( + DiffBeforeClickhouseClusterConfig::Collection { + latest_keeper_membership: None, + .. + }, + Some(after), + ) => Some(ClickhouseClusterConfigDiffTables::added_to_blueprint(after)), + + // No before or after + ( + DiffBeforeClickhouseClusterConfig::Collection { + latest_keeper_membership: None, + .. + }, + None, + ) => None, + + // Before blueprint + after blueprint + ( + DiffBeforeClickhouseClusterConfig::Blueprint(Some(before)), + Some(after), + ) => Some(ClickhouseClusterConfigDiffTables::diff_blueprints(before, after)), + + // Before blueprint only + ( + DiffBeforeClickhouseClusterConfig::Blueprint(Some(before)), + None, + ) => Some(ClickhouseClusterConfigDiffTables::removed_from_blueprint(before)), + + // After blueprint only + ( + DiffBeforeClickhouseClusterConfig::Blueprint(None), + Some(after), + ) => Some(ClickhouseClusterConfigDiffTables::added_to_blueprint(after)), + + // No before or after + (DiffBeforeClickhouseClusterConfig::Blueprint(None), None) => None, + } + } + /// Write out physical disk and zone tables for a given `sled_id` fn write_tables( &self, @@ -976,6 +1416,16 @@ impl<'diff> fmt::Display for BlueprintDiffDisplay<'diff> { writeln!(f, "{}", table)?; } + // Write out clickhouse cluster diff tables + if let Some(tables) = self.make_clickhouse_cluster_config_diff_tables() + { + writeln!(f, "{}", tables.metadata)?; + writeln!(f, "{}", tables.keepers)?; + if let Some(servers) = &tables.servers { + writeln!(f, "{}", servers)?; + } + } + Ok(()) } } diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs index 2b0e4cab6c..19abbb7d1e 100644 --- a/nexus/types/src/deployment/blueprint_display.rs +++ b/nexus/types/src/deployment/blueprint_display.rs @@ -23,6 +23,14 @@ pub mod constants { pub const COCKROACHDB_PRESERVE_DOWNGRADE: &str = "cluster.preserve_downgrade_option"; pub const METADATA_HEADING: &str = "METADATA"; + pub const CLICKHOUSE_CLUSTER_CONFIG_HEADING: &str = + "CLICKHOUSE CLUSTER CONFIG"; + pub const CLICKHOUSE_MAX_USED_SERVER_ID: &str = "max used server id"; + pub const CLICKHOUSE_MAX_USED_KEEPER_ID: &str = "max used keeper id"; + pub const CLICKHOUSE_CLUSTER_NAME: &str = "cluster name"; + pub const CLICKHOUSE_CLUSTER_SECRET: &str = "cluster secret"; + pub const CLICKHOUSE_HIGHEST_SEEN_KEEPER_LEADER_COMMITTED_LOG_INDEX: &str = + "highest seen keeper leader committed log index"; pub const CREATED_BY: &str = "created by"; pub const CREATED_AT: &str = "created at"; pub const INTERNAL_DNS_VERSION: &str = "internal DNS version"; @@ -34,6 +42,7 @@ pub mod constants { pub const NOT_PRESENT_IN_COLLECTION_PARENS: &str = "(not present in collection)"; pub const INVALID_VALUE_PARENS: &str = "(invalid value)"; + pub const GENERATION: &str = "generation"; } use constants::*; @@ -80,6 +89,13 @@ pub enum BpGeneration { Diff { before: Option, after: Option }, } +impl BpGeneration { + // Used when there isn't a corresponding generation + pub fn unknown() -> Self { + BpGeneration::Diff { before: None, after: None } + } +} + impl fmt::Display for BpGeneration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -100,7 +116,7 @@ impl fmt::Display for BpGeneration { } } BpGeneration::Diff { before: None, after: None } => { - write!(f, "Error: unknown generation") + write!(f, "unknown generation") } } } @@ -176,6 +192,9 @@ pub trait BpSledSubtableData { } /// A table specific to a sled resource, such as a zone or disk. +// +// TODO: Rename to `BpTable` since it is also used for blueprint wide tables +// like those relating to `ClickhouseClusterConfig`? pub struct BpSledSubtable { table_name: &'static str, column_names: &'static [&'static str], @@ -334,6 +353,30 @@ impl BpSledSubtableSchema for BpOmicronZonesSubtableSchema { } } +/// The [`BpSledSubtable`] schema for clickhouse keepers +pub struct BpClickhouseKeepersSubtableSchema {} +impl BpSledSubtableSchema for BpClickhouseKeepersSubtableSchema { + fn table_name(&self) -> &'static str { + "clickhouse keepers" + } + + fn column_names(&self) -> &'static [&'static str] { + &["zone id", "keeper id"] + } +} + +/// The [`BpSledSubtable`] schema for clickhouse servers +pub struct BpClickhouseServersSubtableSchema {} +impl BpSledSubtableSchema for BpClickhouseServersSubtableSchema { + fn table_name(&self) -> &'static str { + "clickhouse servers" + } + + fn column_names(&self) -> &'static [&'static str] { + &["zone id", "server id"] + } +} + // An entry in a [`KvListWithHeading`] #[derive(Debug)] pub struct KvPair {