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
61 changes: 61 additions & 0 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1801,4 +1801,65 @@ Describe 'tests for function expressions' {
$errorContent = Get-Content $TestDrive/error.log -Raw
$errorContent | Should -Match $expectedError
}

It 'stateChanged function returns if resource state had changed: <testName>' -TestCases @(
@{ testName = 'state changed'; value = 'new'; object = @{ property = 'Original' }; expected = $true }
@{ testName = 'state unchanged'; value = 'Original'; object = @{ property = 'Original' }; expected = $false }
@{ testName = 'state changed with nested object'; value = 'Original'; object = @{ property = 'New' }; expected = $true }
@{ testName = 'state unchanged with nested object'; value = 'Original'; object = @{ property = 'Original' }; expected = $false }
) {
param($value, $object, $expected)

$config = @{
'$schema' = 'https://aka.ms/dsc/schemas/v3/bundled/config/document.json'
resources = @(
@{
name = 'Test'
type = 'Test/Set'
properties = @{
value = $value
object = $object
}
}
)
outputs = @{
stateChanged = @{
type = 'bool'
value = "[stateChanged(resourceId('Test/Set','Test'))]"
}
}
}

$config = $config | ConvertTo-Json -Depth 10 -Compress
$out = dsc -l trace config set -i $config 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
$out.outputs.stateChanged | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
}

It 'stateChanged function returns false for non-existent resource' {
$config = @{
'$schema' = 'https://aka.ms/dsc/schemas/v3/bundled/config/document.json'
resources = @(
@{
name = 'Test'
type = 'Test/Set'
properties = @{
value = 'new'
}
}
)
outputs = @{
stateChanged = @{
type = 'bool'
value = "[stateChanged(resourceId('NonExistent/Resource','Test'))]"
}
}
}

$config = $config | ConvertTo-Json -Depth 10 -Compress
$null = dsc -l trace config set -i $config 2> $TestDrive/error.log
$errorLog = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 2 -Because $errorLog
$errorLog | Should -BeLike "*Error* No state change information available for resourceId 'NonExistent/Resource:Test' as it has not executed yet or does not exist*"
}
}
113 changes: 113 additions & 0 deletions dsc/tests/dsc_restartRequired.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ Describe '_restartRequired tests' {
- process:
name: anotherProcess
id: 5678
outputs:
system:
type: bool
value: "[restartRequired('system')]"
service:
type: bool
value: "[restartRequired('service', 'sshd')]"
process:
type: bool
value: "[restartRequired('process', 'myProcess')]"
'@
$out = dsc -l trace config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
Expand All @@ -49,6 +59,9 @@ Describe '_restartRequired tests' {
$out.executionInformation.restartRequired[3].process.id | Should -Be 1234
$out.executionInformation.restartRequired[4].process.name | Should -BeExactly 'anotherProcess'
$out.executionInformation.restartRequired[4].process.id | Should -Be 5678
$out.outputs.system | Should -Be $true -Because ($out | ConvertTo-Json -Depth 10)
$out.outputs.service | Should -Be $true -Because ($out | ConvertTo-Json -Depth 10)
$out.outputs.process | Should -Be $true -Because ($out | ConvertTo-Json -Depth 10)
}

It 'invalid item in _restartRequired metadata is a warning' {
Expand All @@ -67,4 +80,104 @@ Describe '_restartRequired tests' {
$out.results[0].executionInformation.restartRequired | Should -BeNullOrEmpty
$out.executionInformation.restartRequired | Should -BeNullOrEmpty
}

It 'restartRequired function returns false for unknown resource: <type>' -TestCases @(
@{ type = 'system' }
@{ type = 'service'; name = ", 'unknown'" }
@{ type = 'process'; name = ", 'unknown'" }
){
param($type, $name)

$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/RestartRequired
properties:
_restartRequired:
- service: myService
- process:
name: myProcess
id: 1234
outputs:
unknown:
type: bool
value: "[restartRequired('$type'$name)]"
"@
$out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$errorContent = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 0 -Because $errorContent
$out.outputs.unknown | Should -Be $false -Because ($out | ConvertTo-Json -Depth 10)
}

It 'restartRequired function returns error if name not specified for: <type>' -TestCases @(
@{ type = 'service' }
@{ type = 'process' }
){
param($type)

$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/RestartRequired
properties:
_restartRequired:
- service: myService
- process:
name: myProcess
id: 1234
outputs:
unknown:
type: bool
value: "[restartRequired('$type')]"
"@
$null = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$errorContent = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 2 -Because $errorContent
$errorContent | Should -BeLike "*ERROR*The 'name' argument is required for kind '$type'*" -Because $errorContent
}

It 'restartRequired function returns error if invalid kind specified' {
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/RestartRequired
properties:
_restartRequired:
- service: myService
- process:
name: myProcess
id: 1234
outputs:
unknown:
type: bool
value: "[restartRequired('invalidKind')]"
"@
$null = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$errorContent = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 2 -Because $errorContent
$errorContent | Should -BeLike "*ERROR*Invalid kind 'invalidKind', must be one of: process, service, system*" -Because $errorContent
}

It 'restartRequired function returns an error if name used with system kind' {
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: test
type: Test/RestartRequired
properties:
_restartRequired:
- system: mySystem
outputs:
unknown:
type: bool
value: "[restartRequired('system', 'nameNotAllowed')]"
"@
$null = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json
$errorContent = Get-Content $TestDrive/error.log -Raw
$LASTEXITCODE | Should -Be 2 -Because $errorContent
$errorContent | Should -BeLike "*ERROR*The 'name' argument is not allowed for kind 'system'*" -Because $errorContent
}
}
13 changes: 13 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,14 @@ description = "Constructs a resource ID from the given type and name"
syntax = "resourceId( <type>, <name> )"
incorrectTypeFormat = "Type argument must contain exactly one slash"

[functions.restartRequired]
description = "Determines if a restart is required. The `name` argument is required for process and service, but not allowed for system."
syntax = "restartRequired( <process | service | system>, [name] )"
constraints = "The `name` argument is required for process and service, but not allowed for system"
invalidKind = "Invalid kind '%{kind}', must be one of: process, service, system"
nameRequired = "The 'name' argument is required for kind '%{kind}'"
nameNotAllowed = "The 'name' argument is not allowed for kind '%{kind}'"

[functions.secret]
description = "Retrieves a secret from a vault"
syntax = "secret( <name>, [vault] )"
Expand Down Expand Up @@ -685,6 +693,11 @@ description = "Checks if a string starts with a specific prefix"
invoked = "startsWith function"
syntax = "startsWith( <string>, <prefix> )"

[functions.stateChanged]
description = "Returns true if the state of the resource has changed since the last execution of the configuration, otherwise returns false. If the resource has not executed yet or does not exist, an error is returned."
syntax = "stateChanged( <resourceId> )"
noStateChangeInformation = "No state change information available for resourceId '%{name}' as it has not executed yet or does not exist"

[functions.stdout]
description = "Returns the standard output from the last executed resource."
syntax = "stdout()"
Expand Down
2 changes: 2 additions & 0 deletions lib/dsc-lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub struct Context {
pub restart_required: Option<Vec<RestartRequired>>,
pub security_context: SecurityContextKind,
pub start_datetime: DateTime<Local>,
pub state_changed: HashMap<String, bool>,
pub stdout: Option<String>,
pub system_root: PathBuf,
pub user_functions: HashMap<String, UserFunctionDefinition>,
Expand Down Expand Up @@ -70,6 +71,7 @@ impl Context {
SecurityContext::User => SecurityContextKind::Restricted,
},
start_datetime: chrono::Local::now(),
state_changed: HashMap::new(),
stdout: None,
system_root: get_default_os_system_root(),
user_functions: HashMap::new(),
Expand Down
3 changes: 2 additions & 1 deletion lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,11 +767,12 @@ impl Configurator {
let resource_result = config_result::ResourceSetResult {
execution_information: Some(execution_information),
metadata: Some(metadata),
name: evaluated_name,
name: evaluated_name.clone(),
resource_type: resource.resource_type.clone(),
result: set_result.clone(),
};
result.results.push(resource_result);
self.context.state_changed.insert(resource_id(&resource.resource_type, &evaluated_name), set_result.is_changed());
progress.set_result(&serde_json::to_value(set_result)?);
progress.write_increment(1);
}
Expand Down
23 changes: 23 additions & 0 deletions lib/dsc-lib/src/dscresources/invoke_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ impl From<TestResult> for SetResult {
}
}

impl SetResult {
#[must_use]
pub fn is_changed(&self) -> bool {
match self {
SetResult::Resource(resource_set_result) => {
if let Some(changed_properties) = &resource_set_result.changed_properties {
!changed_properties.is_empty()
} else {
false
}
},
SetResult::Group(group_set_result) => {
for result in group_set_result {
if result.result.is_changed() {
return true;
}
}
false
}
}
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[serde(deny_unknown_fields)]
#[dsc_repo_schema(base_name = "set.simple", folder_path = "outputs/resource")]
Expand Down
4 changes: 4 additions & 0 deletions lib/dsc-lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ pub mod path;
pub mod range;
pub mod reference;
pub mod resource_id;
pub mod restart_required;
pub mod secret;
pub mod shallow_merge;
pub mod skip;
pub mod starts_with;
pub mod state_changed;
pub mod stdout;
pub mod string;
pub mod take;
Expand Down Expand Up @@ -221,10 +223,12 @@ impl FunctionDispatcher {
Box::new(range::Range{}),
Box::new(reference::Reference{}),
Box::new(resource_id::ResourceId{}),
Box::new(restart_required::RestartRequired{}),
Box::new(secret::Secret{}),
Box::new(shallow_merge::ShallowMerge{}),
Box::new(skip::Skip{}),
Box::new(starts_with::StartsWith{}),
Box::new(state_changed::StateChanged{}),
Box::new(stdout::Stdout{}),
Box::new(string::StringFn{}),
Box::new(sub::Sub{}),
Expand Down
Loading
Loading