diff --git a/build.ps1 b/build.ps1 index 680e9048..9d1ad0a6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -48,6 +48,8 @@ $filesForWindowsPackage = @( 'wmi.resource.ps1', 'windows_baseline.dsc.yaml', 'windows_inventory.dsc.yaml' + 'dsc_default.settings.json', + 'dsc.settings.json' ) $filesForLinuxPackage = @( @@ -62,7 +64,9 @@ $filesForLinuxPackage = @( 'powershell.dsc.resource.json', 'psDscAdapter/', 'RunCommandOnSet.dsc.resource.json', - 'runcommandonset' + 'runcommandonset', + 'dsc_default.settings.json', + 'dsc.settings.json' ) $filesForMacPackage = @( @@ -77,7 +81,9 @@ $filesForMacPackage = @( 'powershell.dsc.resource.json', 'psDscAdapter/', 'RunCommandOnSet.dsc.resource.json', - 'runcommandonset' + 'runcommandonset', + 'dsc_default.settings.json', + 'dsc.settings.json' ) # the list of files other than the binaries which need to be executable diff --git a/dsc/copy_files.txt b/dsc/copy_files.txt new file mode 100644 index 00000000..ea3d9178 --- /dev/null +++ b/dsc/copy_files.txt @@ -0,0 +1,2 @@ +dsc.settings.json +dsc_default.settings.json diff --git a/dsc/dsc.settings.json b/dsc/dsc.settings.json new file mode 100644 index 00000000..19acd63e --- /dev/null +++ b/dsc/dsc.settings.json @@ -0,0 +1,12 @@ +{ + "resourcePath": { + "allowEnvOverride": true, + "appendEnvPath": true, + "directories": [] + }, + "tracing": { + "level": "WARN", + "format": "Default", + "allowOverride": true + } +} diff --git a/dsc/dsc_default.settings.json b/dsc/dsc_default.settings.json new file mode 100644 index 00000000..fe37d458 --- /dev/null +++ b/dsc/dsc_default.settings.json @@ -0,0 +1,14 @@ +{ + "1": { + "resourcePath": { + "allowEnvOverride": true, + "appendEnvPath": true, + "directories": [] + }, + "tracing": { + "level": "WARN", + "format": "Default", + "allowOverride": true + } + } +} diff --git a/dsc/src/args.rs b/dsc/src/args.rs index e572ca31..96ada198 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -4,6 +4,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use clap_complete::Shell; use dsc_lib::dscresources::command_resource::TraceLevel; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum OutputFormat { @@ -12,7 +13,7 @@ pub enum OutputFormat { Yaml, } -#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] +#[derive(Debug, Clone, PartialEq, Eq, ValueEnum, Deserialize)] pub enum TraceFormat { Default, Plaintext, @@ -29,8 +30,8 @@ pub struct Args { pub subcommand: SubCommand, #[clap(short = 'l', long, help = "Trace level to use", value_enum)] pub trace_level: Option, - #[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "default")] - pub trace_format: TraceFormat, + #[clap(short = 'f', long, help = "Trace format to use", value_enum)] + pub trace_format: Option, } #[derive(Debug, PartialEq, Eq, Subcommand)] diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 675b7113..5a2d402f 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -23,10 +23,12 @@ use dsc_lib::{ }, resource_manifest::ResourceManifest }, util::parse_input_to_json, + util::get_setting, }; use jsonschema::Validator; use path_absolutize::Absolutize; use schemars::{schema_for, schema::RootSchema}; +use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; use std::env; @@ -39,7 +41,7 @@ use syntect::{ parsing::SyntaxSet, util::{as_24_bit_terminal_escaped, LinesWithEndings} }; -use tracing::{Level, debug, error, warn, trace}; +use tracing::{Level, debug, error, info, warn, trace}; use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, Layer}; use tracing_indicatif::IndicatifLayer; @@ -55,6 +57,27 @@ pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7; pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT"; pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL"; +#[derive(Deserialize)] +pub struct TracingSetting { + /// Trace level to use - see pub enum `TraceLevel` in `dsc_lib\src\dscresources\command_resource.rs` + level: TraceLevel, + /// Trace format to use - see pub enum `TraceFormat` in `dsc\src\args.rs` + format: TraceFormat, + /// Whether the 'level' can be overrridden by `DSC_TRACE_LEVEL` environment variable + #[serde(rename = "allowOverride")] + allow_override: bool +} + +impl Default for TracingSetting { + fn default() -> TracingSetting { + TracingSetting { + level: TraceLevel::Warn, + format: TraceFormat::Default, + allow_override: true, + } + } +} + /// Get string representation of JSON value. /// /// # Arguments @@ -268,31 +291,76 @@ pub fn write_output(json: &str, format: &Option) { } } -pub fn enable_tracing(trace_level: &Option, trace_format: &TraceFormat) { - let tracing_level = match trace_level { - Some(level) => level, - None => { - // use DSC_TRACE_LEVEL env var if set - match env::var(DSC_TRACE_LEVEL) { - Ok(level) => { - match level.to_ascii_uppercase().as_str() { - "ERROR" => &TraceLevel::Error, - "WARN" => &TraceLevel::Warn, - "INFO" => &TraceLevel::Info, - "DEBUG" => &TraceLevel::Debug, - "TRACE" => &TraceLevel::Trace, - _ => { - warn!("Invalid DSC_TRACE_LEVEL value '{level}', defaulting to 'warn'"); - &TraceLevel::Warn - }, - } +#[allow(clippy::too_many_lines)] +pub fn enable_tracing(trace_level_arg: &Option, trace_format_arg: &Option) { + + let mut policy_is_used = false; + let mut tracing_setting = TracingSetting::default(); + + let default_filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("warn")) + .unwrap_or_default() + .add_directive(Level::WARN.into()); + let default_indicatif_layer = IndicatifLayer::new(); + let default_layer = tracing_subscriber::fmt::Layer::default().with_writer(default_indicatif_layer.get_stderr_writer()); + let default_fmt = default_layer + .with_ansi(true) + .with_level(true) + .boxed(); + let default_subscriber = tracing_subscriber::Registry::default().with(default_fmt).with(default_filter).with(default_indicatif_layer); + let default_guard = tracing::subscriber::set_default(default_subscriber); + + // read setting/policy from files + if let Ok(v) = get_setting("tracing") { + if v.policy != serde_json::Value::Null { + match serde_json::from_value::(v.policy) { + Ok(v) => { + tracing_setting = v; + policy_is_used = true; + }, + Err(e) => { error!("{e}"); } + } + } else if v.setting != serde_json::Value::Null { + match serde_json::from_value::(v.setting) { + Ok(v) => { + tracing_setting = v; }, - Err(_) => &TraceLevel::Warn, + Err(e) => { error!("{e}"); } } } + } else { + error!("Could not read 'tracing' setting"); + } + + // override with DSC_TRACE_LEVEL env var if permitted + if tracing_setting.allow_override { + if let Ok(level) = env::var(DSC_TRACE_LEVEL) { + tracing_setting.level = match level.to_ascii_uppercase().as_str() { + "ERROR" => TraceLevel::Error, + "WARN" => TraceLevel::Warn, + "INFO" => TraceLevel::Info, + "DEBUG" => TraceLevel::Debug, + "TRACE" => TraceLevel::Trace, + _ => { + warn!("Invalid DSC_TRACE_LEVEL value '{level}', defaulting to 'warn'"); + TraceLevel::Warn + } + } + } + } + + // command-line args override setting value, but not policy + if !policy_is_used { + if let Some(v) = trace_level_arg { + tracing_setting.level = v.clone(); + }; + if let Some(v) = trace_format_arg { + tracing_setting.format = v.clone(); + }; }; - let tracing_level = match tracing_level { + // convert to 'tracing' crate type + let tracing_level = match tracing_setting.level { TraceLevel::Error => Level::ERROR, TraceLevel::Warn => Level::WARN, TraceLevel::Info => Level::INFO, @@ -300,14 +368,15 @@ pub fn enable_tracing(trace_level: &Option, trace_format: &TraceForm TraceLevel::Trace => Level::TRACE, }; + // enable tracing let filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("warning")) + .or_else(|_| EnvFilter::try_new("warn")) .unwrap_or_default() .add_directive(tracing_level.into()); let indicatif_layer = IndicatifLayer::new(); let layer = tracing_subscriber::fmt::Layer::default().with_writer(indicatif_layer.get_stderr_writer()); let with_source = tracing_level == Level::DEBUG || tracing_level == Level::TRACE; - let fmt = match trace_format { + let fmt = match tracing_setting.format { TraceFormat::Default => { layer .with_ansi(true) @@ -337,12 +406,14 @@ pub fn enable_tracing(trace_level: &Option, trace_format: &TraceForm let subscriber = tracing_subscriber::Registry::default().with(fmt).with(filter).with(indicatif_layer); + drop(default_guard); if tracing::subscriber::set_global_default(subscriber).is_err() { eprintln!("Unable to set global default tracing subscriber. Tracing is diabled."); } // set DSC_TRACE_LEVEL for child processes env::set_var(DSC_TRACE_LEVEL, tracing_level.to_string().to_ascii_lowercase()); + info!("Trace-level is {:?}", tracing_setting.level); } /// Validate the JSON against the schema. diff --git a/dsc/tests/dsc_settings.tests.ps1 b/dsc/tests/dsc_settings.tests.ps1 new file mode 100644 index 00000000..d78d87fb --- /dev/null +++ b/dsc/tests/dsc_settings.tests.ps1 @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'tests for dsc settings' { + BeforeAll { + + $script:policyFilePath = if ($IsWindows) { + Join-Path $env:ProgramData "dsc" "dsc.settings.json" + } else { + "/etc/dsc/dsc.settings.json" + } + + $script:dscHome = (Get-Command dsc).Path | Split-Path + $script:dscSettingsFilePath = Join-Path $script:dscHome "dsc.settings.json" + $script:dscDefaultSettingsFilePath = Join-Path $script:dscHome "dsc_default.settings.json" + + if ($IsWindows) { #"Setting policy on Linux requires sudo" + $script:policyDirPath = $script:policyFilePath | Split-Path + New-Item -ItemType Directory -Path $script:policyDirPath | Out-Null + } + + #create backups of settings files + $script:dscSettingsFilePath_backup = Join-Path $script:dscHome "dsc.settings.json.backup" + $script:dscDefaultSettingsFilePath_backup = Join-Path $script:dscHome "dsc_default.settings.json.backup" + Copy-Item -Force -Path $script:dscSettingsFilePath -Destination $script:dscSettingsFilePath_backup + Copy-Item -Force -Path $script:dscDefaultSettingsFilePath -Destination $script:dscDefaultSettingsFilePath_backup + } + + AfterAll { + Remove-Item -Force -Path $script:dscSettingsFilePath_backup + Remove-Item -Force -Path $script:dscDefaultSettingsFilePath_backup + if ($IsWindows) { #"Setting policy on Linux requires sudo" + Remove-Item -Recurse -Force -Path $script:policyDirPath + } + } + + BeforeEach { + $script:dscDefaultSettings = Get-Content -Raw -Path $script:dscDefaultSettingsFilePath_backup | ConvertFrom-Json + $script:dscDefaultv1Settings = (Get-Content -Raw -Path $script:dscDefaultSettingsFilePath_backup | ConvertFrom-Json)."1" + } + + AfterEach { + Copy-Item -Force -Path $script:dscSettingsFilePath_backup -Destination $script:dscSettingsFilePath + Copy-Item -Force -Path $script:dscDefaultSettingsFilePath_backup -Destination $script:dscDefaultSettingsFilePath + if ($IsWindows) { #"Setting policy on Linux requires sudo" + Remove-Item -Path $script:policyFilePath -ErrorAction SilentlyContinue + } + } + + It 'ensure a new tracing value in settings has effect' { + + $script:dscDefaultv1Settings."tracing"."level" = "TRACE" + $script:dscDefaultv1Settings | ConvertTo-Json -Depth 90 | Set-Content -Force -Path $script:dscSettingsFilePath + + dsc resource list 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Trace-level is Trace" + } + + It 'ensure a new resource_path value in settings has effect' { + + $script:dscDefaultv1Settings."resourcePath"."directories" = @("TestDir1","TestDir2") + $script:dscDefaultv1Settings | ConvertTo-Json -Depth 90 | Set-Content -Force -Path $script:dscSettingsFilePath + dsc -l debug resource list 2> $TestDrive/tracing.txt + $expectedString = 'Using Resource Path: "TestDir1'+[System.IO.Path]::PathSeparator+'TestDir2' + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly $expectedString + } + + It 'Confirm settings override priorities' { + + if (! $IsWindows) { + Set-ItResult -Skip -Because "Setting policy requires sudo" + return + } + + $script:dscDefaultv1Settings."tracing"."level" = "TRACE" + $script:dscDefaultv1Settings."resourcePath"."directories" = @("PolicyDir") + $script:dscDefaultv1Settings | ConvertTo-Json -Depth 90 | Set-Content -Force -Path $script:policyFilePath + + $script:dscDefaultv1Settings."tracing"."level" = "TRACE" + $script:dscDefaultv1Settings."resourcePath"."directories" = @("SettingsDir") + $script:dscDefaultv1Settings | ConvertTo-Json -Depth 90 | Set-Content -Force -Path $script:dscSettingsFilePath + + $script:dscDefaultSettings."1"."tracing"."level" = "TRACE" + $script:dscDefaultSettings."1"."resourcePath"."directories" = @("Defaultv1SettingsDir") + $script:dscDefaultSettings | ConvertTo-Json -Depth 90 | Set-Content -Force -Path $script:dscDefaultSettingsFilePath + + # ensure policy overrides everything + dsc -l debug resource list 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Trace-level is Trace" + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Using Resource Path: "PolicyDir' + + # without policy, command-line args have priority + Remove-Item -Path $script:policyFilePath + dsc -l debug resource list 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Trace-level is Debug" + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Using Resource Path: "SettingsDir' + + # without policy and command-line args, settings file is used + dsc resource list 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Trace-level is Trace" + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Using Resource Path: "SettingsDir' + + # without policy and command-line args and settings file, the default settings file is used + Remove-Item -Path $script:dscSettingsFilePath + dsc resource list 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly "Trace-level is Trace" + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Using Resource Path: "Defaultv1SettingsDir' + } +} diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index c94abb7d..6d0e26a5 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -11,6 +11,7 @@ use indicatif::ProgressStyle; use linked_hash_map::LinkedHashMap; use regex::RegexBuilder; use semver::Version; +use serde::Deserialize; use std::collections::{BTreeMap, HashSet, HashMap}; use std::env; use std::ffi::OsStr; @@ -18,9 +19,12 @@ use std::fs; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; +use std::str::FromStr; use tracing::{debug, info, trace, warn, warn_span}; use tracing_indicatif::span_ext::IndicatifSpanExt; +use crate::util::get_setting; + pub struct CommandDiscovery { // use BTreeMap so that the results are sorted by the typename, the Vec is sorted by version resources: BTreeMap>, @@ -28,6 +32,28 @@ pub struct CommandDiscovery { adapted_resources: BTreeMap>, } +#[derive(Deserialize)] +pub struct ResourcePathSetting { + /// whether to allow overriding with the `DSC_RESOURCE_PATH` environment variable + #[serde(rename = "allowEnvOverride")] + allow_env_override: bool, + /// whether to append the PATH environment variable to the list of resource directories + #[serde(rename = "appendEnvPath")] + append_env_path: bool, + /// array of directories that DSC should search for non-built-in resources + directories: Vec +} + +impl Default for ResourcePathSetting { + fn default() -> ResourcePathSetting { + ResourcePathSetting { + allow_env_override: true, + append_env_path: true, + directories: vec![], + } + } +} + impl CommandDiscovery { pub fn new() -> CommandDiscovery { CommandDiscovery { @@ -37,31 +63,74 @@ impl CommandDiscovery { } } + fn get_resource_path_setting() -> Result + { + if let Ok(v) = get_setting("resourcePath") { + // if there is a policy value defined - use it; otherwise use setting value + if v.policy != serde_json::Value::Null { + match serde_json::from_value::(v.policy) { + Ok(v) => { + return Ok(v); + }, + Err(e) => { return Err(DscError::Setting(format!("{e}"))); } + } + } else if v.setting != serde_json::Value::Null { + match serde_json::from_value::(v.setting) { + Ok(v) => { + return Ok(v); + }, + Err(e) => { return Err(DscError::Setting(format!("{e}"))); } + } + } + } + + Err(DscError::Setting("Could not read 'resourcePath' setting".to_string())) + } + fn get_resource_paths() -> Result, DscError> { + let mut resource_path_setting = ResourcePathSetting::default(); + + match Self::get_resource_path_setting() { + Ok(v) => { + resource_path_setting = v; + }, + Err(e) => { + debug!("{e}"); + } + } + let mut using_custom_path = false; + let mut paths: Vec = vec![]; - // try DSC_RESOURCE_PATH env var first otherwise use PATH - let path_env = if let Some(value) = env::var_os("DSC_RESOURCE_PATH") { + let dsc_resource_path = env::var_os("DSC_RESOURCE_PATH"); + if resource_path_setting.allow_env_override && dsc_resource_path.is_some(){ + let value = dsc_resource_path.unwrap(); debug!("Using DSC_RESOURCE_PATH: {:?}", value.to_string_lossy()); using_custom_path = true; - value + paths.append(&mut env::split_paths(&value).collect::>()); } else { - trace!("DSC_RESOURCE_PATH not set, trying PATH"); - match env::var_os("PATH") { - Some(value) => { - trace!("Original PATH: {:?}", value.to_string_lossy()); - value - }, - None => { - return Err(DscError::Operation("Failed to get PATH environment variable".to_string())); + for p in resource_path_setting.directories { + let v = PathBuf::from_str(&p); + paths.push(v.unwrap_or_default()); + } + + if resource_path_setting.append_env_path { + debug!("Appending PATH to resourcePath"); + match env::var_os("PATH") { + Some(value) => { + trace!("Original PATH: {:?}", value.to_string_lossy()); + paths.append(&mut env::split_paths(&value).collect::>()); + }, + None => { + return Err(DscError::Operation("Failed to get PATH environment variable".to_string())); + } } } - }; + } - let mut paths = env::split_paths(&path_env).collect::>(); // remove duplicate entries - let mut uniques = HashSet::new(); + let mut uniques: HashSet = HashSet::new(); paths.retain(|e|uniques.insert((*e).clone())); // if exe home is not already in PATH env var then add it to env var and list of searched paths @@ -75,13 +144,16 @@ impl CommandDiscovery { paths.push(exe_home_pb); if let Ok(new_path) = env::join_paths(paths.clone()) { - debug!("Using PATH: {:?}", new_path.to_string_lossy()); env::set_var("PATH", &new_path); } } } }; + if let Ok(final_resource_path) = env::join_paths(paths.clone()) { + debug!("Using Resource Path: {:?}", final_resource_path.to_string_lossy()); + } + Ok(paths) } } diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index f570988b..e2c3b631 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -117,4 +117,7 @@ pub enum DscError { #[error("YAML: {0}")] Yaml(#[from] serde_yaml::Error), + + #[error("Setting: {0}")] + Setting(String), } diff --git a/dsc_lib/src/util.rs b/dsc_lib/src/util.rs index a5bbcccf..177d9549 100644 --- a/dsc_lib/src/util.rs +++ b/dsc_lib/src/util.rs @@ -3,6 +3,26 @@ use crate::dscerror::DscError; use serde_json::Value; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::path::Path; +use std::env; +use tracing::debug; + +pub struct DscSettingValue { + pub setting: Value, + pub policy: Value, +} + +impl Default for DscSettingValue { + fn default() -> DscSettingValue { + DscSettingValue { + setting: Value::Null, + policy: Value::Null, + } + } +} /// Return JSON string whether the input is JSON or YAML /// @@ -37,3 +57,103 @@ pub fn parse_input_to_json(value: &str) -> Result { } } } + +/// Will search setting files for the specified setting. +/// Performance implication: Use this function economically as every call opens/reads several config files. +/// TODO: cache the config +/// +/// # Arguments +/// +/// * `value_name` - The name of the setting. +/// +/// # Errors +/// +/// Will return `Err` if could not find requested setting. +pub fn get_setting(value_name: &str) -> Result { + + const SETTINGS_FILE_NAME: &str = "dsc.settings.json"; + // Note that default settings file has root nodes as settings schema version that is specific to this version of dsc + const DEFAULT_SETTINGS_FILE_NAME: &str = "dsc_default.settings.json"; + const DEFAULT_SETTINGS_SCHEMA_VERSION: &str = "1"; + + let mut result: DscSettingValue = DscSettingValue::default(); + let mut settings_file_path : PathBuf; + + if let Some(exe_home) = env::current_exe()?.parent() { + // First, get setting from the default settings file + settings_file_path = exe_home.join(DEFAULT_SETTINGS_FILE_NAME); + if let Ok(v) = load_value_from_json(&settings_file_path, DEFAULT_SETTINGS_SCHEMA_VERSION) { + if let Some(n) = v.get(value_name) { + result.setting = n.clone(); + debug!("Found setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } + } else { + debug!("Did not find setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } + + // Second, get setting from the active settings file overwriting previous value + settings_file_path = exe_home.join(SETTINGS_FILE_NAME); + if let Ok(v) = load_value_from_json(&settings_file_path, value_name) { + result.setting = v; + debug!("Found setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } else { + debug!("Did not find setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } + } else { + debug!("Can't get dsc executable path"); + } + + // Third, get setting from the policy + settings_file_path = PathBuf::from(get_settings_policy_file_path()); + if let Ok(v) = load_value_from_json(&settings_file_path, value_name) { + result.policy = v; + debug!("Found setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } else { + debug!("Did not find setting '{}' in {}", &value_name, settings_file_path.to_string_lossy()); + } + + if (result.setting == serde_json::Value::Null) && + (result.policy == serde_json::Value::Null) { + return Err(DscError::NotSupported(format!("Could not find '{value_name}' in settings").to_string())); + } + + Ok(result) +} + +fn load_value_from_json(path: &PathBuf, value_name: &str) -> Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let root: serde_json::Value = match serde_json::from_reader(reader) { + Ok(j) => j, + Err(err) => { + return Err(DscError::Json(err)); + } + }; + + if let Some(r) = root.as_object() { + for (key, value) in r { + if *key == value_name { + return Ok(value.clone()) + } + } + } + + Err(DscError::NotSupported(value_name.to_string())) +} + +#[cfg(target_os = "windows")] +fn get_settings_policy_file_path() -> String +{ + // $env:ProgramData+"\dsc\dsc.settings.json" + // This location is writable only by admins, but readable by all users + let Ok(local_program_data_path) = std::env::var("ProgramData") else { return String::new(); }; + Path::new(&local_program_data_path).join("dsc").join("dsc.settings.json").display().to_string() +} + +#[cfg(not(target_os = "windows"))] +fn get_settings_policy_file_path() -> String +{ + // "/etc/dsc/dsc.settings.json" + // This location is writable only by admins, but readable by all users + Path::new("/etc").join("dsc").join("dsc.settings.json").display().to_string() +}