v0.3.2-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 20
|
||||
// version: 21
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
||||
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
||||
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
|
||||
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC), Store and Wallet documents map explicitly to their runtime consumer
|
||||
//! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
|
||||
//! JSON/`.env` persistence.
|
||||
|
||||
@@ -27,6 +27,7 @@ mod persistence;
|
||||
mod profile;
|
||||
mod registry;
|
||||
mod sensitivity;
|
||||
mod store;
|
||||
mod transport;
|
||||
mod wallet;
|
||||
|
||||
@@ -164,6 +165,10 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard Store configuration document.
|
||||
pub use self::registry::DEFAULT_STD_STORE_FILENAME;
|
||||
/// Default physical filename for the standard Store JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_STORE_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -182,6 +187,8 @@ pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard Store JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_STORE;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
@@ -190,6 +197,8 @@ pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard Store configuration document.
|
||||
pub use self::registry::FILE_ID_STD_STORE;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
@@ -204,6 +213,8 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
||||
pub use self::sensitivity::ResolvedConfigJson;
|
||||
/// One resolved Config string preserving real/safe representations and provenance.
|
||||
pub use self::sensitivity::ResolvedConfigText;
|
||||
/// Effective standard Store configuration mapped to backend-neutral Store settings.
|
||||
pub use self::store::ResolvedStoreConfig;
|
||||
/// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
|
||||
pub use self::transport::ResolvedTransportConfig;
|
||||
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -17,6 +17,10 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME: &str = "std.offchain_transport.json";
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME: &str = "std.offchain_transport.schema.json";
|
||||
/// Default physical filename for the standard Store configuration document.
|
||||
pub const DEFAULT_STD_STORE_FILENAME: &str = "std.store.json";
|
||||
/// Default physical filename for the standard Store JSON Schema document.
|
||||
pub const DEFAULT_STD_STORE_SCHEMA_FILENAME: &str = "std.store.schema.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -35,6 +39,8 @@ pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT: &str = "schema.std.offchain_transport";
|
||||
/// Logical file identifier for the standard Store JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_STORE: &str = "schema.std.store";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
@@ -43,6 +49,8 @@ pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub const FILE_ID_STD_OFFCHAIN_TRANSPORT: &str = "cfg.std.offchain_transport";
|
||||
/// Logical file identifier for the standard Store configuration document.
|
||||
pub const FILE_ID_STD_STORE: &str = "cfg.std.store";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
@@ -214,6 +222,22 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_STORE,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_STD_STORE_FILENAME,
|
||||
std::option::Option::Some(FILE_ID_SCHEMA_STD_STORE),
|
||||
);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_schema =
|
||||
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_STORE, ConfigFileKind::Schema, DEFAULT_STD_STORE_SCHEMA_FILENAME, std::option::Option::None);
|
||||
let store_schema = match store_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let transport = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_TRANSPORT,
|
||||
ConfigFileKind::Config,
|
||||
@@ -254,6 +278,8 @@ impl ConfigFileRegistry {
|
||||
logging_schema,
|
||||
offchain_transport,
|
||||
offchain_transport_schema,
|
||||
store,
|
||||
store_schema,
|
||||
transport,
|
||||
transport_schema,
|
||||
wallet,
|
||||
|
||||
264
crates/ksp-config-lib/src/store.rs
Normal file
264
crates/ksp-config-lib/src/store.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
// file: crates/ksp-config-lib/src/store.rs
|
||||
// version: 1
|
||||
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn profile_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,
|
||||
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: bool,
|
||||
migration_lock_timeout_ms: u64,
|
||||
migration_timeout_ms: u64,
|
||||
}
|
||||
|
||||
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 {
|
||||
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 = ksp_store_lib::PostgresBootstrapSettings::new(
|
||||
source.postgres.bootstrap.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),
|
||||
);
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(source.postgres.connection_uri, pool, tls_mode, bootstrap);
|
||||
let settings = ksp_store_lib::StoreSettings::new(
|
||||
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(),
|
||||
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;
|
||||
Reference in New Issue
Block a user