// file: crates/ksp-app-config-desk/src/environment.rs // version: 4 //! Safe Config environment reports and `.env` management projections for Config Desk. use ts_rs::TS; // rust-rules: trait-import /// Safe desired/effective environment view exposed to the ordinary Config Desk frontend. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/environment/ConfigEnvironmentReportDto.ts")] pub(crate) struct ConfigEnvironmentReportDto { /// Supported KSP/KSPB variable name. pub(crate) variable_name: String, /// Namespace family derived from the variable name without inspecting its value. pub(crate) namespace: String, /// `public`, `internal` or `secret`. pub(crate) sensitivity: String, /// Persisted `.env` value using Config's safe/redacted representation. pub(crate) desired_safe_value: std::option::Option, /// Currently effective process-or-`.env` value using Config's safe/redacted representation. pub(crate) effective_safe_value: std::option::Option, /// Winning external source: `process`, `dotenv`, or `none`. pub(crate) effective_source: String, /// Whether the persisted `.env` value is shadowed by the inherited process environment. pub(crate) shadowed_by_process_environment: bool, } /// Safe result metadata for one `.env` mutation. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/environment/ConfigEnvironmentChangeDto.ts")] pub(crate) struct ConfigEnvironmentChangeDto { /// Variable targeted by the operation; never its value. pub(crate) variable_name: String, /// Stable operation label: `set` or `remove`. pub(crate) operation: String, /// Sensitivity classified by Config from the variable namespace. pub(crate) sensitivity: String, /// Whether the persisted `.env` source bytes changed. pub(crate) source_changed: bool, /// Whether the effective process-or-`.env` value changed for the running process. pub(crate) effective_changed: bool, /// Whether the resulting `.env` entry is shadowed by inherited process environment. pub(crate) shadowed_by_process_environment: bool, /// Whether consumers holding an environment snapshot must reload to observe the effective change. pub(crate) reload_required: bool, } #[derive(Clone, Copy)] enum EnvironmentMutationOperation { Set, Remove, } impl EnvironmentMutationOperation { const fn label(self) -> &'static str { return match self { Self::Set => "set", Self::Remove => "remove", }; } } /// Returns the safe environment report owned by Config. pub(crate) fn environment_report(state: &crate::AppState) -> ksp_core_lib::Result> { return report_from_management(state.config_management()); } /// Creates or replaces one `.env` entry exclusively through ConfigManagement. pub(crate) fn set_environment_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result { return set_value_from_management(state.config_management(), variable_name, value); } /// Removes one `.env` entry exclusively through ConfigManagement. pub(crate) fn remove_environment_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result { return remove_value_from_management(state.config_management(), variable_name); } fn report_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result> { let reports = management.environment_report(); let reports = match reports { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut result = std::vec::Vec::::with_capacity(reports.len()); let mut process_count = 0usize; let mut dotenv_count = 0usize; let mut shadowed_count = 0usize; let mut secret_count = 0usize; for report in reports { let source = report.effective_source(); match source { std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Process) => process_count += 1, std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::DotEnv) => dotenv_count += 1, std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Fallback) | std::option::Option::None => {}, } if report.shadowed_by_process_environment() { shadowed_count += 1; } if report.sensitivity() == ksp_config_lib::ConfigSensitivity::Secret { secret_count += 1; } result.push(ConfigEnvironmentReportDto { variable_name: report.variable_name().to_owned(), namespace: namespace_label(report.variable_name()).to_owned(), sensitivity: sensitivity_label(report.sensitivity()).to_owned(), desired_safe_value: report.desired_safe_value().map(str::to_owned), effective_safe_value: report.effective_safe_value().map(str::to_owned), effective_source: environment_source_label(source).to_owned(), shadowed_by_process_environment: report.shadowed_by_process_environment(), }); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_ENVIRONMENT, variable_count = result.len(), process_count, dotenv_count, shadowed_count, secret_count, "Config environment report evaluated" ); return std::result::Result::Ok(result); } fn set_value_from_management( management: &ksp_config_lib::ConfigManagement, variable_name: &str, value: &str, ) -> ksp_core_lib::Result { let sensitivity = mutation_sensitivity(variable_name); let sensitivity = match sensitivity { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let report = management.set_dotenv_value(variable_name, value); let report = match report { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(mutation_result(variable_name, EnvironmentMutationOperation::Set, sensitivity, report)); } fn remove_value_from_management(management: &ksp_config_lib::ConfigManagement, variable_name: &str) -> ksp_core_lib::Result { let sensitivity = mutation_sensitivity(variable_name); let sensitivity = match sensitivity { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let report = management.remove_dotenv_value(variable_name); let report = match report { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(mutation_result(variable_name, EnvironmentMutationOperation::Remove, sensitivity, report)); } fn mutation_result( variable_name: &str, operation: EnvironmentMutationOperation, sensitivity: ksp_config_lib::ConfigSensitivity, report: ksp_config_lib::ConfigEnvironmentChangeReport, ) -> ConfigEnvironmentChangeDto { let result = ConfigEnvironmentChangeDto { variable_name: variable_name.to_owned(), operation: operation.label().to_owned(), sensitivity: sensitivity_label(sensitivity).to_owned(), source_changed: report.source_changed(), effective_changed: report.effective_changed(), shadowed_by_process_environment: report.shadowed_by_process_environment(), reload_required: report.reload_required(), }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_ENVIRONMENT, variable_name = result.variable_name.as_str(), operation = result.operation.as_str(), sensitivity = result.sensitivity.as_str(), source_changed = result.source_changed, effective_changed = result.effective_changed, shadowed_by_process_environment = result.shadowed_by_process_environment, reload_required = result.reload_required, "Config .env mutation completed" ); return result; } fn mutation_sensitivity(variable_name: &str) -> ksp_core_lib::Result { return ksp_config_lib::ConfigSensitivity::from_variable_name(variable_name); } const fn environment_source_label(source: std::option::Option) -> &'static str { return match source { std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Process) => "process", std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::DotEnv) => "dotenv", std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Fallback) => "fallback", std::option::Option::None => "none", }; } const fn sensitivity_label(sensitivity: ksp_config_lib::ConfigSensitivity) -> &'static str { return match sensitivity { ksp_config_lib::ConfigSensitivity::Public => "public", ksp_config_lib::ConfigSensitivity::Internal => "internal", ksp_config_lib::ConfigSensitivity::Secret => "secret", }; } fn namespace_label(variable_name: &str) -> &'static str { if variable_name.starts_with("KSPB_SECRET_") { return "KSPB_SECRET"; } if variable_name.starts_with("KSPB_PUBLIC_") { return "KSPB_PUBLIC"; } if variable_name.starts_with("KSPB_") { return "KSPB"; } if variable_name.starts_with("KSP_SECRET_") { return "KSP_SECRET"; } if variable_name.starts_with("KSP_PUBLIC_") { return "KSP_PUBLIC"; } return "KSP"; } #[cfg(test)] #[path = "../unit_tests/environment.rs"] mod tests;