Skip to content

Commit

Permalink
Merge pull request #574 from anmenaga/issue_282
Browse files Browse the repository at this point in the history
DSC meta configuration
  • Loading branch information
SteveL-MSFT authored Nov 8, 2024
2 parents c209290 + 4085e32 commit 2918d67
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 43 deletions.
10 changes: 8 additions & 2 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ $filesForWindowsPackage = @(
'wmi.resource.ps1',
'windows_baseline.dsc.yaml',
'windows_inventory.dsc.yaml'
'dsc_default.settings.json',
'dsc.settings.json'
)

$filesForLinuxPackage = @(
Expand All @@ -62,7 +64,9 @@ $filesForLinuxPackage = @(
'powershell.dsc.resource.json',
'psDscAdapter/',
'RunCommandOnSet.dsc.resource.json',
'runcommandonset'
'runcommandonset',
'dsc_default.settings.json',
'dsc.settings.json'
)

$filesForMacPackage = @(
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions dsc/copy_files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dsc.settings.json
dsc_default.settings.json
12 changes: 12 additions & 0 deletions dsc/dsc.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"resourcePath": {
"allowEnvOverride": true,
"appendEnvPath": true,
"directories": []
},
"tracing": {
"level": "WARN",
"format": "Default",
"allowOverride": true
}
}
14 changes: 14 additions & 0 deletions dsc/dsc_default.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"1": {
"resourcePath": {
"allowEnvOverride": true,
"appendEnvPath": true,
"directories": []
},
"tracing": {
"level": "WARN",
"format": "Default",
"allowOverride": true
}
}
}
7 changes: 4 additions & 3 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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<TraceLevel>,
#[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<TraceFormat>,
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
Expand Down
117 changes: 94 additions & 23 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -268,46 +291,92 @@ pub fn write_output(json: &str, format: &Option<OutputFormat>) {
}
}

pub fn enable_tracing(trace_level: &Option<TraceLevel>, 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<TraceLevel>, trace_format_arg: &Option<TraceFormat>) {

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::<TracingSetting>(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::<TracingSetting>(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,
TraceLevel::Debug => Level::DEBUG,
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)
Expand Down Expand Up @@ -337,12 +406,14 @@ pub fn enable_tracing(trace_level: &Option<TraceLevel>, 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.
Expand Down
109 changes: 109 additions & 0 deletions dsc/tests/dsc_settings.tests.ps1
Original file line number Diff line number Diff line change
@@ -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'
}
}
Loading

0 comments on commit 2918d67

Please sign in to comment.