306 lines
13 KiB
Rust
306 lines
13 KiB
Rust
// file: crates/ksp-config-lib/src/store.rs
|
|
// version: 3
|
|
|
|
/// Effective standard Store configuration mapped to `ksp_store_lib::StoreSettings`.
|
|
pub struct ResolvedStoreConfig {
|
|
effective: crate::ResolvedConfigJson,
|
|
file_id: crate::ConfigFileId,
|
|
profile_id: String,
|
|
selection_source: crate::ConfigProfileSelectionSource,
|
|
settings: ksp_store_lib::StoreSettings,
|
|
source_path: std::path::PathBuf,
|
|
}
|
|
|
|
impl ResolvedStoreConfig {
|
|
/// Returns the detailed environment-resolved Config view.
|
|
#[must_use]
|
|
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
|
|
return &self.effective;
|
|
}
|
|
|
|
/// Returns the logical Config file identifier used by this runtime configuration.
|
|
#[must_use]
|
|
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
|
return &self.file_id;
|
|
}
|
|
|
|
/// Returns the selected standard Store profile identifier.
|
|
///
|
|
/// For `std.store`, the profile identifier is also the stable named Store target identifier.
|
|
#[must_use]
|
|
pub fn profile_id(&self) -> &str {
|
|
return self.profile_id.as_str();
|
|
}
|
|
|
|
/// Returns the selected named Store target identifier.
|
|
#[must_use]
|
|
pub fn target_id(&self) -> &str {
|
|
return self.profile_id.as_str();
|
|
}
|
|
|
|
/// Returns the source that selected the standard Store profile.
|
|
#[must_use]
|
|
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
|
return self.selection_source;
|
|
}
|
|
|
|
/// Borrows the backend-neutral Store settings without exposing the connection URI.
|
|
#[must_use]
|
|
pub const fn settings(&self) -> &ksp_store_lib::StoreSettings {
|
|
return &self.settings;
|
|
}
|
|
|
|
/// Consumes the resolved Config and returns the Store-owned runtime settings.
|
|
#[must_use]
|
|
pub fn into_settings(self) -> ksp_store_lib::StoreSettings {
|
|
return self.settings;
|
|
}
|
|
|
|
/// Returns the physical source Config document path.
|
|
#[must_use]
|
|
pub fn source_path(&self) -> &std::path::Path {
|
|
return self.source_path.as_path();
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for ResolvedStoreConfig {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
return formatter
|
|
.debug_struct("ResolvedStoreConfig")
|
|
.field("effective", &self.effective)
|
|
.field("file_id", &self.file_id)
|
|
.field("profile_id", &self.profile_id)
|
|
.field("selection_source", &self.selection_source)
|
|
.field("settings", &self.settings)
|
|
.field("source_path", &self.source_path)
|
|
.finish();
|
|
}
|
|
}
|
|
|
|
impl crate::ConfigDocumentEngine {
|
|
/// Loads `std.store`, resolves one profile/environment and maps it to backend-neutral Store settings.
|
|
pub fn load_resolved_store_config(
|
|
&self,
|
|
requested_profile: std::option::Option<&str>,
|
|
environment: &crate::ConfigEnvironment,
|
|
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_STORE);
|
|
let file_id = match file_id {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let profile = self.load_resolved_profile(&file_id, requested_profile);
|
|
let profile = match profile {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return resolve_store_profile(&profile, environment);
|
|
}
|
|
|
|
/// Maps an already resolved `cfg.std.store` profile while preserving its selection provenance.
|
|
pub fn resolve_store_config_profile(
|
|
&self,
|
|
profile: &crate::ResolvedConfigProfile,
|
|
environment: &crate::ConfigEnvironment,
|
|
) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
|
if profile.file_id().as_str() != crate::FILE_ID_STD_STORE {
|
|
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Store document"));
|
|
}
|
|
let descriptor = self.registry().descriptor(profile.file_id());
|
|
if let std::result::Result::Err(error) = descriptor {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return resolve_store_profile(profile, environment);
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EffectiveStoreSource {
|
|
backend: String,
|
|
format_version: u32,
|
|
network: String,
|
|
postgres: EffectivePostgresSource,
|
|
profile_id: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EffectivePostgresSource {
|
|
bootstrap: EffectivePostgresBootstrapSource,
|
|
connection_uri: String,
|
|
pool: EffectivePostgresPoolSource,
|
|
shutdown_timeout_ms: u64,
|
|
tls: EffectivePostgresTlsSource,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EffectivePostgresPoolSource {
|
|
connect_timeout_ms: u64,
|
|
create_timeout_ms: u64,
|
|
max_connections: u32,
|
|
recycle_timeout_ms: u64,
|
|
wait_timeout_ms: u64,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EffectivePostgresTlsSource {
|
|
mode: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EffectivePostgresBootstrapSource {
|
|
auto_migrate: std::option::Option<bool>,
|
|
migration_lock_timeout_ms: u64,
|
|
migration_timeout_ms: u64,
|
|
schema_autocreate: std::option::Option<bool>,
|
|
schema_autoupdate: std::option::Option<bool>,
|
|
}
|
|
|
|
fn resolve_store_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedStoreConfig> {
|
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Store Config profile");
|
|
let effective = profile.resolve_effective_environment_detailed(environment);
|
|
let effective = match effective {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let provenance = validate_connection_uri_provenance(&effective, profile);
|
|
if let std::result::Result::Err(error) = provenance {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let source = serde_json::from_value::<EffectiveStoreSource>(effective.value().clone());
|
|
let source = match source {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(
|
|
effective_error(profile, "effective Store Config cannot be decoded into the runtime adapter contract").with_source(error),
|
|
);
|
|
},
|
|
};
|
|
if source.format_version != 1 && source.format_version != 2 {
|
|
return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported"));
|
|
}
|
|
if source.profile_id != profile.profile_id() {
|
|
return std::result::Result::Err(effective_error(profile, "effective Store profile_id does not match the selected profile"));
|
|
}
|
|
if source.backend != "postgres" {
|
|
return std::result::Result::Err(effective_error(profile, "effective Store backend is unsupported").with_context("backend", source.backend));
|
|
}
|
|
let tls_mode = match source.postgres.tls.mode.as_str() {
|
|
"disabled" => ksp_store_lib::PostgresTlsMode::Disabled,
|
|
"verify_full" => ksp_store_lib::PostgresTlsMode::VerifyFull,
|
|
_ => return std::result::Result::Err(effective_error(profile, "effective Store PostgreSQL TLS mode is unsupported")),
|
|
};
|
|
let pool = ksp_store_lib::PostgresPoolSettings::new(
|
|
source.postgres.pool.max_connections,
|
|
std::time::Duration::from_millis(source.postgres.pool.connect_timeout_ms),
|
|
std::time::Duration::from_millis(source.postgres.pool.wait_timeout_ms),
|
|
std::time::Duration::from_millis(source.postgres.pool.create_timeout_ms),
|
|
std::time::Duration::from_millis(source.postgres.pool.recycle_timeout_ms),
|
|
);
|
|
let bootstrap = match source.format_version {
|
|
1 => {
|
|
let auto_migrate =
|
|
match (source.postgres.bootstrap.auto_migrate, source.postgres.bootstrap.schema_autocreate, source.postgres.bootstrap.schema_autoupdate) {
|
|
(std::option::Option::Some(value), std::option::Option::None, std::option::Option::None) => value,
|
|
_ => return std::result::Result::Err(effective_error(profile, "effective Store V1 bootstrap policy is invalid")),
|
|
};
|
|
ksp_store_lib::PostgresBootstrapSettings::new(
|
|
auto_migrate,
|
|
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
|
|
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
|
|
)
|
|
},
|
|
2 => {
|
|
let (schema_autocreate, schema_autoupdate) =
|
|
match (source.postgres.bootstrap.auto_migrate, source.postgres.bootstrap.schema_autocreate, source.postgres.bootstrap.schema_autoupdate) {
|
|
(std::option::Option::None, std::option::Option::Some(autocreate), std::option::Option::Some(autoupdate)) => (autocreate, autoupdate),
|
|
_ => return std::result::Result::Err(effective_error(profile, "effective Store V2 bootstrap policy is invalid")),
|
|
};
|
|
ksp_store_lib::PostgresBootstrapSettings::with_schema_policy(
|
|
schema_autocreate,
|
|
schema_autoupdate,
|
|
std::time::Duration::from_millis(source.postgres.bootstrap.migration_timeout_ms),
|
|
std::time::Duration::from_millis(source.postgres.bootstrap.migration_lock_timeout_ms),
|
|
)
|
|
},
|
|
_ => return std::result::Result::Err(effective_error(profile, "effective Store format_version is unsupported")),
|
|
};
|
|
let network = ksp_store_lib::RawNetworkId::new(source.network);
|
|
let network = match network {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(effective_error(profile, "effective Store network identifier is invalid")),
|
|
};
|
|
let postgres = ksp_store_lib::PostgresStoreSettings::new(source.postgres.connection_uri, pool, tls_mode, bootstrap);
|
|
let settings = ksp_store_lib::StoreSettings::new(
|
|
network,
|
|
ksp_store_lib::StoreBackendSettings::Postgres(postgres),
|
|
std::time::Duration::from_millis(source.postgres.shutdown_timeout_ms),
|
|
);
|
|
if let std::result::Result::Err(error) = settings.validate() {
|
|
return std::result::Result::Err(store_contract_error(profile, &error));
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
profile_id = profile.profile_id(),
|
|
network = settings.network().as_str(),
|
|
backend = settings.backend_kind().code(),
|
|
"mapped standard Store Config to Store settings"
|
|
);
|
|
return std::result::Result::Ok(ResolvedStoreConfig {
|
|
effective,
|
|
file_id: profile.file_id().clone(),
|
|
profile_id: profile.profile_id().to_owned(),
|
|
selection_source: profile.selection_source(),
|
|
settings,
|
|
source_path: profile.path().to_path_buf(),
|
|
});
|
|
}
|
|
|
|
fn validate_connection_uri_provenance(effective: &crate::ResolvedConfigJson, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<()> {
|
|
let provenance = match effective.provenance_at("/postgres/connection_uri") {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI provenance is unavailable")),
|
|
};
|
|
let mut has_secret_environment = false;
|
|
for item in provenance {
|
|
let variable_name = match item.variable_name() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
|
let sensitivity = match sensitivity {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if !sensitivity.is_secret() {
|
|
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI may reference only secret environment variables"));
|
|
}
|
|
has_secret_environment = true;
|
|
}
|
|
if !has_secret_environment {
|
|
return std::result::Result::Err(effective_error(profile, "Store PostgreSQL connection URI requires secret environment provenance"));
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn store_contract_error(profile: &crate::ResolvedConfigProfile, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
|
return effective_error(profile, "effective Store settings fail the Store runtime contract")
|
|
.with_context("store_error_domain", error.code().domain())
|
|
.with_context("store_error_code", error.code().code());
|
|
}
|
|
|
|
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
|
|
.with_context("file_id", profile.file_id().as_str())
|
|
.with_context("profile_id", profile.profile_id())
|
|
.with_context("reason", reason);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/store.rs"]
|
|
mod tests;
|