Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ proc-macro2 = { version = "1.0" }
quote = { version = "1.0" }
# dsc, dsc-lib
regex = { version = "1.12" }
# registry, dsc-lib-registry
# registry, dsc-lib, dsc-lib-registry, dsctest
registry = { version = "1.3" }
# dsc
rmcp = { version = "0.16" }
Expand Down Expand Up @@ -229,7 +229,7 @@ tree-sitter = { version = "0.26" }
tree-sitter-language = { version = "0.1" }
# dsc-lib, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config
tree-sitter-rust = { version = "0.24" }
# registry, dsc-lib-registry
# registry, dsc-lib-registry, dsctest
utfx = { version = "0.1" }
# dsc-lib
uuid = { version = "1.21", features = ["v4"] }
Expand Down
91 changes: 91 additions & 0 deletions dsc/tests/dsc_metadata.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,95 @@ Describe 'metadata tests' {
(Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property '_restartRequired' which contains invalid value: ``[{`"invalid`":`"item`"}]*"
$out.results[0].metadata._restartRequired | Should -BeNullOrEmpty
}

It '_refreshEnv refreshes the environment variables for subsequent resources' {
if ($IsWindows) {
Remove-Item -Path "HKCU:\Environment\myTestVariable" -ErrorAction Ignore
}

$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: create variable
type: Test/RefreshEnv
properties:
name: myTestVariable
value: myTestValue
- name: return variable
type: Microsoft.DSC.Transitional/PowerShellScript
properties:
SetScript: |
if ($IsWindows) {
$env:myTestVariable
}
'@
try {
$out = dsc -l trace config set -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$errorLogContent = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 0 -Because $errorLogContent
if ($IsWindows) {
$errorLogContent | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed*" -Because $errorLogContent
$out.results[1].result.afterState.output | Should -BeExactly 'myTestValue' -Because ($out | ConvertTo-Json -Depth 10)
} else {
$errorLogContent | Should -BeLike "*INFO*Resource returned '_refreshEnv' which is ignored on non-Windows platforms*" -Because $errorLogContent
}
} finally {
if ($IsWindows) {
Remove-Item -Path "HKCU:\Environment\myTestVariable" -ErrorAction Ignore
}
}
}

It '_refreshEnv handles PATH correctly' -Skip:(!$IsWindows) {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: add to path
type: Test/RefreshEnv
properties:
name: PATH
value: C:\MyTestPath
- name: return path
type: Microsoft.DSC.Transitional/PowerShellScript
properties:
SetScript: |
$env:PATH
'@
$oldUserPath = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::User)
try {
$out = dsc -l trace config set -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
$out.results[1].result.afterState.output.Split(';') | Should -Contain 'C:\MyTestPath'
} finally {
[System.Environment]::SetEnvironmentVariable('PATH', $oldUserPath, [System.EnvironmentVariableTarget]::User)
}
}

It '_refreshEnv does not trigger for <operation>' -Skip:(!$IsWindows) -TestCases @(
@{ operation = 'get' }
@{ operation = 'test' }
) {
param($operation)

$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/RefreshEnv
properties:
name: myTestVariable
value: myTestValue
- name: return variable
type: Microsoft.DSC.Transitional/PowerShellScript
properties:
SetScript: |
if ($IsWindows) {
$env:myTestVariable
}
'@
$out = dsc -l trace config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
(Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*Resource returned '_refreshEnv' which indicates environment variable refresh is needed but current operation is '$operation' which is not 'set', so ignoring*" -Because (Get-Content $TestDrive/error.log -Raw)
$out.results[0].result.afterState.output | Should -Not -Be 'myTestValue'
}
}
3 changes: 3 additions & 0 deletions lib/dsc-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ dsc-lib-security_context = { workspace = true }
dsc-lib-jsonschema = { workspace = true }
tree-sitter-dscexpression = { workspace = true }

[target.'cfg(windows)'.dependencies]
registry = { workspace = true }

[dev-dependencies]
serde_yaml = { workspace = true }
# Helps review complex comparisons, like schemas
Expand Down
10 changes: 10 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ skippingResourceDiscovery = "Skipping resource discovery due to 'resourceDiscove
securityContextInMetadataDeprecated = "Using 'Microsoft.DSC' metadata to specify required security context is deprecated. Please use the 'securityContext' directive in the configuration document instead. See https://github.com/PowerShell/DSC/issues/1369 for more details."
conflictingSecurityContext = "Conflicting security context specified in configuration document: metadata '%{metadata}' and directive '%{directive}'"
versionNotSatisfied = "Configuration requires DSC version '%{required_version}', but the current version is '%{current_version}'"
invalidRegistryHive = "Invalid registry hive '%{hive}'"
metadataRefreshEnvIgnored = "Resource returned '_refreshEnv' which is ignored on non-Windows platforms"
metadataRefreshEnvFound = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed"
metadataRefreshEnvOnlyAffectsSet = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but current operation is '%{operation}' which is not 'set', so ignoring"
metadataRefreshEnvNoOperationContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no operation context is available"
metadataRefreshEnvNoContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no context is available"

[configure.parameters]
importingParametersFromComplexInput = "Importing parameters from complex input"
Expand Down Expand Up @@ -776,6 +782,10 @@ setting = "Setting"
invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{resource}'"
resourceMissingDirectory = "Resource is missing 'directory' field."
resourceMissingPath = "Resource is missing 'path' field."
registryHive = "Failed to open registry hive"
registryKey = "Failed to access registry key"
registryIterator = "Failed to iterate over registry values"
utf16Conversion = "Failed to convert UTF-16 bytes to string"

[progress]
failedToSerialize = "Failed to serialize progress JSON: %{json}"
Expand Down
12 changes: 12 additions & 0 deletions lib/dsc-lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ pub enum Operation {
Export,
}

impl Display for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operation_str = match self {
Operation::Get => "get",
Operation::Set => "set",
Operation::Test => "test",
Operation::Export => "export",
};
write!(f, "{operation_str}")
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(transform = idiomaticize_string_enum)]
Expand Down
6 changes: 4 additions & 2 deletions lib/dsc-lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

use chrono::{DateTime, Local};
use crate::{configure::config_doc::{ExecutionKind, UserFunctionDefinition}, extensions::dscextension::DscExtension};
use crate::{configure::config_doc::{ExecutionKind, Operation, UserFunctionDefinition}, extensions::dscextension::DscExtension};
use dsc_lib_security_context::{get_security_context, SecurityContext};
use serde_json::{Map, Value};
use std::{collections::HashMap, path::PathBuf};
Expand All @@ -18,13 +18,14 @@ pub enum ProcessMode {
UserFunction,
}

#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Context {
pub copy: HashMap<String, i64>,
pub copy_current_loop_name: String,
pub dsc_version: Option<String>,
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub operation: Option<Operation>,
pub outputs: Map<String, Value>,
pub parameters: HashMap<String, (Value, DataType)>,
pub process_expressions: bool,
Expand All @@ -49,6 +50,7 @@ impl Context {
dsc_version: None,
execution_type: ExecutionKind::Actual,
extensions: Vec::new(),
operation: None,
outputs: Map::new(),
parameters: HashMap::new(),
process_expressions: true,
Expand Down
77 changes: 76 additions & 1 deletion lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,53 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu
}
}
}
if key == "_refreshEnv" && value.as_bool() == Some(true) {
if let Some(ref context) = context {
if let Some(operation) = &context.operation {
if *operation != Operation::Set {
info!("{}", t!("configure.mod.metadataRefreshEnvOnlyAffectsSet", operation = operation));
continue;
}
} else {
debug!("{}", t!("configure.mod.metadataRefreshEnvNoOperationContext"));
continue;
}
} else {
debug!("{}", t!("configure.mod.metadataRefreshEnvNoContext"));
continue;
}
#[cfg(not(windows))]
{
info!("{}", t!("configure.mod.metadataRefreshEnvIgnored"));
continue;
}
#[cfg(windows)]
{
info!("{}", t!("configure.mod.metadataRefreshEnvFound"));
// rebuild the environment variables from Windows registry
// read the environment variables from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
// overlay with HKCU\Environment for user level variables except for PATH which is prefixed instead of overlayed
let mut env_vars = read_windows_registry("HKLM", r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")?;
let user_env_vars = read_windows_registry("HKCU", r"Environment")?;
for (key, value) in user_env_vars {
if key == "PATH" {
if let Some(system_path) = env_vars.get(&key) {
trace!("Prefixing user PATH '{value}' to system PATH '{system_path}'");
env_vars.insert(key, format!("{};{}", value, system_path));
} else {
trace!("System PATH not found, using user PATH '{value}' as PATH");
env_vars.insert(key, value.to_string());
}
} else {
env_vars.insert(key, value.to_string());
}
}
// set the current process env vars to the new values
for (key, value) in env_vars {
std::env::set_var(&key.to_string(), &value);
}
}
}
metadata.other.insert(key.clone(), value.clone());
}
} else {
Expand All @@ -307,6 +354,31 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu
Ok(())
}

#[cfg(windows)]
fn read_windows_registry(hive: &str, path: &str) -> Result<HashMap<String, String>, DscError> {
use registry::Hive;
let hive = match hive {
"HKLM" => Hive::LocalMachine,
"HKCU" => Hive::CurrentUser,
_ => return Err(DscError::Parser(t!("configure.mod.invalidRegistryHive", hive = hive).to_string())),
};
let reg_key = hive.open(path, registry::Security::Read)?;
let mut result: HashMap<String, String> = HashMap::new();
for value in reg_key.values() {
let value = value?;
// env vars on Windows aren't case-sensitive, so we use uppercase to keep them consistent
let name = value.name().to_string()?.to_uppercase();
let data = match value.data() {
// for env var we only need to handle string and expand string types, other types will be ignored
registry::Data::String(s) => s.to_string()?,
registry::Data::ExpandString(s) => s.to_string()?,
_ => continue,
};
result.insert(name, data);
}
Ok(result)
}

impl Configurator {
/// Create a new `Configurator` instance.
///
Expand Down Expand Up @@ -396,6 +468,7 @@ impl Configurator {
/// This function will return an error if the underlying resource fails.
pub fn invoke_get(&mut self) -> Result<ConfigurationGetResult, DscError> {
let mut result = ConfigurationGetResult::new();
self.context.operation = Some(Operation::Get);
let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?;
let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?;
let discovery = &mut self.discovery.clone();
Expand Down Expand Up @@ -488,6 +561,7 @@ impl Configurator {
#[allow(clippy::too_many_lines)]
pub fn invoke_set(&mut self, skip_test: bool) -> Result<ConfigurationSetResult, DscError> {
let mut result = ConfigurationSetResult::new();
self.context.operation = Some(Operation::Set);
let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?;
let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?;
let discovery = &mut self.discovery.clone();
Expand All @@ -508,7 +582,6 @@ impl Configurator {
};
let properties = self.get_properties(&resource, &dsc_resource.kind)?;
debug!("resource_type {}", &resource.resource_type);

// see if the properties contains `_exist` and is false
let exist = match &properties {
Some(property_map) => {
Expand Down Expand Up @@ -681,6 +754,7 @@ impl Configurator {
/// This function will return an error if the underlying resource fails.
pub fn invoke_test(&mut self) -> Result<ConfigurationTestResult, DscError> {
let mut result = ConfigurationTestResult::new();
self.context.operation = Some(Operation::Test);
let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &mut self.context)?;
let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?;
let discovery = &mut self.discovery.clone();
Expand Down Expand Up @@ -769,6 +843,7 @@ impl Configurator {
/// This function will return an error if the underlying resource fails.
pub fn invoke_export(&mut self) -> Result<ConfigurationExportResult, DscError> {
let mut result = ConfigurationExportResult::new();
self.context.operation = Some(Operation::Export);
let mut conf = config_doc::Configuration::new();
conf.metadata.clone_from(&self.config.metadata);

Expand Down
Loading