232 lines
10 KiB
Rust
232 lines
10 KiB
Rust
// 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<String>,
|
|
/// Currently effective process-or-`.env` value using Config's safe/redacted representation.
|
|
pub(crate) effective_safe_value: std::option::Option<String>,
|
|
/// 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<std::vec::Vec<ConfigEnvironmentReportDto>> {
|
|
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<ConfigEnvironmentChangeDto> {
|
|
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<ConfigEnvironmentChangeDto> {
|
|
return remove_value_from_management(state.config_management(), variable_name);
|
|
}
|
|
|
|
fn report_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
|
|
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::<ConfigEnvironmentReportDto>::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<ConfigEnvironmentChangeDto> {
|
|
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<ConfigEnvironmentChangeDto> {
|
|
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<ksp_config_lib::ConfigSensitivity> {
|
|
return ksp_config_lib::ConfigSensitivity::from_variable_name(variable_name);
|
|
}
|
|
|
|
const fn environment_source_label(source: std::option::Option<ksp_config_lib::ConfigEnvironmentSource>) -> &'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;
|