diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index c3f9ddc7b..696ed2af8 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -248,26 +248,25 @@ Describe 'PowerShell adapter resource tests' { } It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( - @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.DSC/PowerShell' } - @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } - @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } - @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/PowerShell' } - @{ Operation = 'get'; metadata = 'Ignored' } - @{ Operation = 'set'; metadata = 'Ignored' } - @{ Operation = 'test'; metadata = 'Ignored' } + @{ Operation = 'get'; directive = 'requireAdapter: '; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'set'; directive = 'requireAdapter: '; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'test'; directive = 'requireAdapter: '; adapter = 'Microsoft.DSC/PowerShell' } + @{ Operation = 'get'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'set'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'test'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/PowerShell' } + @{ Operation = 'get'; directive = '' } + @{ Operation = 'set'; directive = '' } + @{ Operation = 'test'; directive = '' } ) { - param($Operation, $metadata, $adapter) + param($Operation, $directive, $adapter) $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Class-resource Info type: TestClassResource/TestClassResource - metadata: - ${metadata}: - requireAdapter: $adapter + directives: + $directive$adapter properties: Name: 'TestClassResource1' HashTableProp: @@ -290,7 +289,7 @@ Describe 'PowerShell adapter resource tests' { $out.results[0].result.inDesiredState | Should -BeFalse -Because $text } } - if ($metadata -eq 'Microsoft.DSC') { + if ($directive -eq 'requireAdapter: ') { "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) } } diff --git a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 index f6b42fb37..9857a7734 100644 --- a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 +++ b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 @@ -232,26 +232,25 @@ resources: } It 'Config calling PS Resource directly works for with metadata and adapter ' -TestCases @( - @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } - @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } - @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Windows/WindowsPowerShell' } - @{ Operation = 'get'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } - @{ Operation = 'set'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } - @{ Operation = 'test'; metadata = 'Microsoft.DSC'; adapter = 'Microsoft.Adapter/WindowsPowerShell' } - @{ Operation = 'get'; metadata = 'Ignored' } - @{ Operation = 'set'; metadata = 'Ignored' } - @{ Operation = 'test'; metadata = 'Ignored' } + @{ Operation = 'get'; directive = 'requireAdapter: '; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'set'; directive = 'requireAdapter: '; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'test'; directive = 'requireAdapter: '; adapter = 'Microsoft.Windows/WindowsPowerShell' } + @{ Operation = 'get'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'set'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'test'; directive = 'requireAdapter: '; adapter = 'Microsoft.Adapter/WindowsPowerShell' } + @{ Operation = 'get'; directive = ''; adapter = '' } + @{ Operation = 'set'; directive = ''; adapter = '' } + @{ Operation = 'test'; directive = ''; adapter = '' } ) { - param($Operation, $metadata, $adapter) + param($Operation, $directive, $adapter) $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Class-resource Info type: PSClassResource/PSClassResource - metadata: - ${metadata}: - requireAdapter: $adapter + directives: + $directive$adapter properties: Name: TestInstance Credential: @@ -277,7 +276,7 @@ resources: $out.results[0].result.inDesiredState | Should -BeTrue -Because $text } } - if ($metadata -eq 'Microsoft.DSC') { + if ($directive -eq 'requireAdapter: ') { "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) } diff --git a/dsc/examples/require_admin.yaml b/dsc/examples/require_admin.yaml index 9650e8e5f..2f71a0369 100644 --- a/dsc/examples/require_admin.yaml +++ b/dsc/examples/require_admin.yaml @@ -2,9 +2,8 @@ # note that the resource doesn't require admin, but this will fail to even try to run the # config if the user is not root or elevated as administrator $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -metadata: - Microsoft.DSC: - securityContext: elevated +directives: + securityContext: elevated resources: - name: os type: Microsoft/OSInfo diff --git a/dsc/examples/require_nonadmin.yaml b/dsc/examples/require_nonadmin.yaml index a41ed2855..2c1b4f852 100644 --- a/dsc/examples/require_nonadmin.yaml +++ b/dsc/examples/require_nonadmin.yaml @@ -1,9 +1,8 @@ # example showing use of specific metadata to indicate this config requires admin to run # this will fail to even try to run the config if the user is root or elevated as administrator $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -metadata: - Microsoft.DSC: - securityContext: restricted +directives: + securityContext: restricted resources: - name: os type: Microsoft/OSInfo diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index e8cb8035b..323205f27 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -107,9 +107,8 @@ Describe 'Tests for adapter support' { type: Test/Invalid properties: output: '1' - metadata: - Microsoft.DSC: - requireAdapter: InvalidAdapter/Invalid + directives: + requireAdapter: InvalidAdapter/Invalid "@ $out = dsc config get -i $config_yaml 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log | Out-String) @@ -126,16 +125,14 @@ Describe 'Tests for adapter support' { type: TestClassResource/TestClassResource properties: Name: 'Hello' - metadata: - Microsoft.DSC: - requireAdapter: Microsoft.DSC/PowerShell + directives: + requireAdapter: Microsoft.DSC/PowerShell - name: Test2 type: TestClassResource/TestClassResource properties: Name: 'Bye' - metadata: - Microsoft.DSC: - requireAdapter: Microsoft.Adapter/PowerShell + directives: + requireAdapter: Microsoft.Adapter/PowerShell '@ $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index a0e6b6256..a6809ec60 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -301,21 +301,20 @@ Describe 'tests for resource discovery' { } } - It 'Resource discovery can be set to ' -TestCases @( - @{ namespace = 'Microsoft.DSC'; mode = 'preDeployment' } - @{ namespace = 'Microsoft.DSC'; mode = 'duringDeployment' } - @{ namespace = 'Ignore'; mode = 'ignore' } + It 'Resource discovery directive can be set to ' -TestCases @( + @{ mode = 'resourceDiscovery: preDeployment' } + @{ mode = 'resourceDiscovery: duringDeployment' } + @{ mode = '' } ) { - param($namespace, $mode) + param($mode) $guid = (New-Guid).Guid.Replace('-', '') $manifestPath = Join-Path (Split-Path (Get-Command dscecho -ErrorAction Stop).Source -Parent) echo.dsc.resource.json $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/manifest.json - metadata: - ${namespace}: - resourceDiscovery: $mode + directives: + $mode resources: - type: Test/CopyResource name: This should be found and executed @@ -329,7 +328,7 @@ Describe 'tests for resource discovery' { "@ $out = dsc -l trace config get -i $config_yaml 2> "$testdrive/tracing.txt" $traceLog = Get-Content -Raw -Path "$testdrive/tracing.txt" - if ($mode -ne 'duringDeployment') { + if ($mode -notlike '*duringDeployment') { $LASTEXITCODE | Should -Be 2 $out | Should -BeNullOrEmpty $traceLog | Should -Match "ERROR.*?Resource not found: Test/$guid" diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 index e4643914c..0700954c1 100644 --- a/dsc/tests/dsc_group.tests.ps1 +++ b/dsc/tests/dsc_group.tests.ps1 @@ -6,6 +6,14 @@ Describe 'Group resource tests' { $out = (dsc config get -f $PSScriptRoot/../examples/groups.dsc.yaml -o yaml | Out-String).Trim() $LASTEXITCODE | Should -Be 0 $out | Should -BeLike @' +executionInformation: + duration: PT*S + endDatetime: * + executionType: actual + operation: get + securityContext: * + startDatetime: * + version: 3* metadata: Microsoft.DSC: duration: PT*S @@ -16,13 +24,17 @@ metadata: startDatetime: * version: 3* results: -- metadata: +- executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: First Group type: Microsoft.DSC/Group result: - - metadata: + - executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: First @@ -30,13 +42,17 @@ results: result: actualState: output: First - - metadata: + - executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: Nested Group type: Microsoft.DSC/Group result: - - metadata: + - executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: Nested First @@ -44,7 +60,9 @@ results: result: actualState: output: Nested First - - metadata: + - executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: Nested Second @@ -52,13 +70,17 @@ results: result: actualState: output: Nested Second -- metadata: +- executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: Last Group type: Microsoft.DSC/Group result: - - metadata: + - executionInformation: + duration: * + metadata: Microsoft.DSC: duration: * name: Last @@ -68,7 +90,7 @@ results: output: Last messages: `[`] hadErrors: false -'@ +'@ -Because $out } } diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 5a6c81988..ae9cff5f3 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -189,6 +189,22 @@ Describe 'metadata tests' { $out.metadata.'Microsoft.DSC'.restartRequired[3].process.id | Should -Be 1234 $out.metadata.'Microsoft.DSC'.restartRequired[4].process.name | Should -BeExactly 'anotherProcess' $out.metadata.'Microsoft.DSC'.restartRequired[4].process.id | Should -Be 5678 + $out.results[0].executionInformation.restartRequired.count | Should -Be 2 + $out.results[0].executionInformation.restartRequired[0].system | Should -BeExactly 'mySystem' + $out.results[0].executionInformation.restartRequired[1].service | Should -BeExactly 'myService' + $out.results[1].executionInformation.restartRequired.count | Should -Be 1 + $out.results[1].executionInformation.restartRequired[0].service | Should -BeExactly 'sshd' + $out.results[2].executionInformation.restartRequired.count | Should -Be 2 + $out.results[2].executionInformation.restartRequired[0].process.name | Should -BeExactly 'myProcess' + $out.results[2].executionInformation.restartRequired[0].process.id | Should -Be 1234 + $out.executionInformation.restartRequired.count | Should -Be 5 + $out.executionInformation.restartRequired[0].system | Should -BeExactly 'mySystem' + $out.executionInformation.restartRequired[1].service | Should -BeExactly 'myService' + $out.executionInformation.restartRequired[2].service | Should -BeExactly 'sshd' + $out.executionInformation.restartRequired[3].process.name | Should -BeExactly 'myProcess' + $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 } It 'invalid item in _restartRequired metadata is a warning' { diff --git a/dsc/tests/dsc_securitycontext.tests.ps1 b/dsc/tests/dsc_securitycontext.tests.ps1 index 19509827b..d05a49156 100644 --- a/dsc/tests/dsc_securitycontext.tests.ps1 +++ b/dsc/tests/dsc_securitycontext.tests.ps1 @@ -23,6 +23,29 @@ Describe 'Tests for configuration security context metadata' { } } + It 'Require admin with warning deprecated' { + $configYaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +metadata: + Microsoft.DSC: + securityContext: elevated +resources: +- name: os + type: Microsoft/OSInfo + properties: {} +'@ + $out = dsc config get -i $configYaml 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + $errorLog | Should -BeLike "*Using 'Microsoft.DSC' metadata to specify required security context is deprecated. Please use the 'securityContext' directive in the configuration document instead.*" + if ($isAdmin) { + $LASTEXITCODE | Should -Be 0 + $out | Should -Not -BeNullOrEmpty + } + else { + $LASTEXITCODE | Should -Be 2 + } + } + It 'Require non-admin' { $out = dsc config get -f $PSScriptRoot/../examples/require_nonadmin.yaml 2>$null if ($isAdmin) { @@ -33,4 +56,80 @@ Describe 'Tests for configuration security context metadata' { $out | Should -Not -BeNullOrEmpty } } + + It 'Require admin with conflicting metadata and directive' { + $configYaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +metadata: + Microsoft.DSC: + securityContext: elevated +directives: + securityContext: restricted +resources: +- name: os + type: Microsoft/OSInfo + properties: {} +'@ + $null = dsc config get -i $configYaml 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + $errorLog | Should -BeLike "*Conflicting security context specified in configuration document: metadata 'elevated' and directive 'restricted'*" + $LASTEXITCODE | Should -Be 2 + } + + It 'Require non-admin with warning deprecated' { + $configYaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +metadata: + Microsoft.DSC: + securityContext: restricted +resources: +- name: os + type: Microsoft/OSInfo + properties: {} +'@ + $out = dsc config get -i $configYaml 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + $errorLog | Should -BeLike "*Using 'Microsoft.DSC' metadata to specify required security context is deprecated. Please use the 'securityContext' directive in the configuration document instead.*" + if ($isAdmin) { + $LASTEXITCODE | Should -Be 2 + } + else { + $LASTEXITCODE | Should -Be 0 + $out | Should -Not -BeNullOrEmpty + } + } + + It 'Resource with directive security context for ' -TestCases @( + @{ operation = 'get'; property = 'actualState' } + @{ operation = 'set'; property = 'afterState' } + @{ operation = 'test'; property = 'actualState' } + @{ operation = 'export' } + ) { + param($operation, $property) + $configYaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: echo + type: Microsoft.DSC.Debug/Echo + properties: + output: 'Hello' + directives: + securityContext: elevated +'@ + $out = dsc config $operation -i $configYaml 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + if ($isAdmin) { + $LASTEXITCODE | Should -Be 0 + $result = $out | ConvertFrom-Json + if ($operation -eq 'export') { + $result.resources[0].properties.output | Should -BeExactly 'Hello' -Because $out + } else { + $result.results[0].result.$property.output | Should -BeExactly 'Hello' -Because $out + } + } + else { + $errorLog | Should -BeLike "*ERROR*Security context: Elevated security context required*" + $LASTEXITCODE | Should -Be 2 + } + } } diff --git a/dsc/tests/dsc_version.tests.ps1 b/dsc/tests/dsc_version.tests.ps1 index b3e7a7e93..3ef528367 100644 --- a/dsc/tests/dsc_version.tests.ps1 +++ b/dsc/tests/dsc_version.tests.ps1 @@ -17,4 +17,40 @@ Describe 'tests for metadata versioning' { $dscVersion = (dsc --version).Split(" ")[1] $version | Should -Be $dscVersion } -} \ No newline at end of file + + It 'returns error if configuration requires higher DSC version' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + directives: + version: 999.0.0 + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: 'Hello, World!' +"@ + $null = $config_yaml | dsc config get -f - 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + $errorLog | Should -BeLike "*Validation*Configuration requires DSC version '999.0.0', but the current version is '*" + $LASTEXITCODE | Should -Be 2 + } + + It 'returns no error if DSC version satisfies configuration requirement' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + directives: + version: '>=3.1' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: 'Hello, World!' +"@ + $out = $config_yaml | dsc config get -f - 2>$testdrive/error.log + $errorLog = Get-Content -Path $testdrive/error.log -Raw + $errorLog | Should -BeNullOrEmpty + $LASTEXITCODE | Should -Be 0 + $result = $out | ConvertFrom-Json + $result.results[0].result.actualState.output | Should -BeExactly 'Hello, World!' -Because $out + } +} diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index ba838f8b6..7bab1e669 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -153,12 +153,15 @@ Describe 'whatif tests' { $what_if_result.hadErrors | Should -BeFalse $what_if_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'whatIf' $what_if_result.results[0].metadata.whatIf[0] | Should -BeExactly 'Delete what-if message 1' + $what_if_result.results[0].executionInformation.whatIf[0] | Should -BeExactly 'Delete what-if message 1' $what_if_result.results[0].metadata.whatIf[1] | Should -BeExactly 'Delete what-if message 2' + $what_if_result.results[0].executionInformation.whatIf[1] | Should -BeExactly 'Delete what-if message 2' $set_result = $config_yaml | dsc config set -f - | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $set_result.hadErrors | Should -BeFalse $set_result.metadata.'Microsoft.DSC'.executionType | Should -BeExactly 'actual' $set_result.results[0].metadata.whatIf | Should -BeNullOrEmpty + $set_result.results[0].executionInformation.whatIf | Should -BeNullOrEmpty } It 'Synthetic what-if for delete resource works' { diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 8b3779cba..60f5cf4ff 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -85,6 +85,9 @@ secureOutputSkipped = "Secure output '%{name}' is skipped" outputTypeNotMatch = "Output '%{name}' type does not match expected type '%{expected_type}'" copyNotSupported = "Copy for output '%{name}' is currently not supported" skippingResourceDiscovery = "Skipping resource discovery due to 'resourceDiscovery' mode set to 'DuringDeployment'" +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}'" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index fd3eb680e..1deec7156 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -16,17 +16,28 @@ use crate::{schemas::{ #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] #[schemars(transform = idiomaticize_string_enum)] -#[dsc_repo_schema(base_name = "securityContext", folder_path = "metadata/Microsoft.DSC")] +#[dsc_repo_schema(base_name = "securityContext", folder_path = "executionInformation")] pub enum SecurityContextKind { Current, Elevated, Restricted, } +impl Display for SecurityContextKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let context_str = match self { + SecurityContextKind::Current => "current", + SecurityContextKind::Elevated => "elevated", + SecurityContextKind::Restricted => "restricted", + }; + write!(f, "{context_str}") + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] #[schemars(transform = idiomaticize_string_enum)] -#[dsc_repo_schema(base_name = "operation", folder_path = "metadata/Microsoft.DSC")] +#[dsc_repo_schema(base_name = "operation", folder_path = "executionInformation")] pub enum Operation { Get, Set, @@ -37,7 +48,7 @@ pub enum Operation { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] #[schemars(transform = idiomaticize_string_enum)] -#[dsc_repo_schema(base_name = "executionType", folder_path = "metadata/Microsoft.DSC")] +#[dsc_repo_schema(base_name = "executionType", folder_path = "executionInformation")] pub enum ExecutionKind { Actual, WhatIf, @@ -53,7 +64,7 @@ pub struct Process { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] #[schemars(transform = idiomaticize_externally_tagged_enum)] -#[dsc_repo_schema(base_name = "restartRequired", folder_path = "metadata/Microsoft.DSC")] +#[dsc_repo_schema(base_name = "restartRequired", folder_path = "executionInformation")] pub enum RestartRequired { System(String), Service(String), @@ -62,12 +73,22 @@ pub enum RestartRequired { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] -#[dsc_repo_schema(base_name = "resourceDiscovery", folder_path = "metadata/Microsoft.DSC")] +#[dsc_repo_schema(base_name = "resourceDiscovery", folder_path = "directive")] pub enum ResourceDiscoveryMode { PreDeployment, DuringDeployment, } +impl Display for ResourceDiscoveryMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mode_str = match self { + ResourceDiscoveryMode::PreDeployment => "preDeployment", + ResourceDiscoveryMode::DuringDeployment => "duringDeployment", + }; + write!(f, "{mode_str}") + } +} + #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MicrosoftDscMetadata { @@ -83,12 +104,6 @@ pub struct MicrosoftDscMetadata { /// The operation being performed #[serde(skip_serializing_if = "Option::is_none")] pub operation: Option, - /// Specify specific adapter type used for implicit operations - #[serde(skip_serializing_if = "Option::is_none")] - pub require_adapter: Option, - /// Indicates if resources are discovered pre-deployment or during deployment - #[serde(skip_serializing_if = "Option::is_none")] - pub resource_discovery: Option, /// Indicates what needs to be restarted after the configuration operation #[serde(skip_serializing_if = "Option::is_none")] pub restart_required: Option>, @@ -126,6 +141,94 @@ impl MicrosoftDscMetadata { } } +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(rename_all = "camelCase")] +#[dsc_repo_schema(base_name = "executionInformation", folder_path = "config")] +pub struct ExecutionInformation { + /// The duration of the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + /// The end time of the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub end_datetime: Option, + /// The type of execution + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_type: Option, + /// The operation being performed + #[serde(skip_serializing_if = "Option::is_none")] + pub operation: Option, + /// Indicates what needs to be restarted after the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub restart_required: Option>, + /// Copy loop context for resources expanded from copy loops + #[serde(rename = "copyLoops", skip_serializing_if = "Option::is_none")] + pub copy_loops: Option>, + /// The security context used for the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub security_context: Option, + /// The start time of the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub start_datetime: Option, + /// Version of DSC + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + /// Information about what-if operations performed during this execution, if any + #[serde(skip_serializing_if = "Option::is_none")] + pub what_if: Option, +} + +impl ExecutionInformation { + #[must_use] + pub fn new() -> Self { + Self { + duration: None, + end_datetime: None, + execution_type: None, + operation: None, + restart_required: None, + copy_loops: None, + security_context: None, + start_datetime: None, + version: None, + what_if: None, + } + } + + pub fn new_with_duration(start: &DateTime, end: &DateTime) -> Self { + Self { + duration: Some(end.signed_duration_since(*start).to_string()), + ..Self::new() + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(rename_all = "camelCase")] +#[dsc_repo_schema(base_name = "directive", folder_path = "config")] +pub struct ConfigDirective { + /// Indicates if resources are discovered pre-deployment or during deployment + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_discovery: Option, + /// The required security context of the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub security_context: Option, + /// Required version of DSC + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(rename_all = "camelCase")] +#[dsc_repo_schema(base_name = "directive", folder_path = "resource")] +pub struct ResourceDirective { + /// Specify specific adapter type used for implicit operations + #[serde(skip_serializing_if = "Option::is_none")] + pub require_adapter: Option, + /// The required security context of the configuration operation + #[serde(skip_serializing_if = "Option::is_none")] + pub security_context: Option, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[dsc_repo_schema(base_name = "document.metadata", folder_path = "config")] pub struct Metadata { @@ -199,6 +302,10 @@ pub struct Configuration { #[serde(rename = "contentVersion")] pub content_version: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub directives: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub functions: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, @@ -361,6 +468,10 @@ pub struct Resource { #[serde(skip_serializing_if = "Option::is_none")] pub comments: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub directives: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, #[serde(rename = "dependsOn", skip_serializing_if = "Option::is_none")] #[schemars(regex(pattern = r"^\[resourceId\(\s*'[a-zA-Z0-9\.]+/[a-zA-Z0-9]+'\s*,\s*'[a-zA-Z0-9 ]+'\s*\)]$"))] @@ -399,6 +510,8 @@ impl Configuration { Self { schema: Self::default_schema_id_uri(), content_version: Some("1.0.0".to_string()), + directives: None, + execution_information: None, metadata: None, parameters: None, resources: Vec::new(), @@ -416,6 +529,8 @@ impl Resource { resource_type: FullyQualifiedTypeName::default(), name: String::new(), depends_on: None, + directives: None, + execution_information: None, kind: None, properties: None, metadata: None, diff --git a/lib/dsc-lib/src/configure/config_result.rs b/lib/dsc-lib/src/configure/config_result.rs index 31fae6e8b..3278e11cb 100644 --- a/lib/dsc-lib/src/configure/config_result.rs +++ b/lib/dsc-lib/src/configure/config_result.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; -use crate::configure::config_doc::{Configuration, Metadata}; +use crate::configure::config_doc::{Configuration, ExecutionInformation, Metadata}; use crate::schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum}; use crate::types::FullyQualifiedTypeName; @@ -34,6 +34,8 @@ pub struct ResourceMessage { #[serde(deny_unknown_fields)] #[dsc_repo_schema(base_name = "get.full", folder_path = "outputs/resource")] pub struct ResourceGetResult { + #[serde(rename = "executionInformation", skip_serializing_if = "Option::is_none")] + pub execution_information: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, pub name: String, @@ -45,6 +47,7 @@ pub struct ResourceGetResult { impl From for ResourceGetResult { fn from(test_result: ResourceTestResult) -> Self { Self { + execution_information: None, metadata: None, name: test_result.name, resource_type: test_result.resource_type, @@ -54,13 +57,13 @@ impl From for ResourceGetResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema(base_name = "get", folder_path = "outputs/config")] pub struct ConfigurationGetResult { + pub execution_information: Option, pub metadata: Option, pub results: Vec, pub messages: Vec, - #[serde(rename = "hadErrors")] pub had_errors: bool, #[serde(skip_serializing_if = "Option::is_none")] pub outputs: Option>, @@ -70,6 +73,7 @@ impl ConfigurationGetResult { #[must_use] pub fn new() -> Self { Self { + execution_information: None, metadata: None, results: Vec::new(), messages: Vec::new(), @@ -92,6 +96,7 @@ impl From for ConfigurationGetResult { results.push(result.into()); } Self { + execution_information: None, metadata: None, results, messages: test_result.messages, @@ -105,6 +110,8 @@ impl From for ConfigurationGetResult { #[serde(deny_unknown_fields)] #[dsc_repo_schema(base_name = "set.full", folder_path = "outputs/resource")] pub struct ResourceSetResult { + #[serde(rename = "executionInformation", skip_serializing_if = "Option::is_none")] + pub execution_information: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, pub name: String, @@ -116,6 +123,7 @@ pub struct ResourceSetResult { impl From for ResourceSetResult { fn from(test_result: ResourceTestResult) -> Self { Self { + execution_information: None, metadata: None, name: test_result.name, resource_type: test_result.resource_type, @@ -146,13 +154,13 @@ impl Default for GroupResourceSetResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema(base_name = "set", folder_path = "outputs/config")] pub struct ConfigurationSetResult { + pub execution_information: Option, pub metadata: Option, pub results: Vec, pub messages: Vec, - #[serde(rename = "hadErrors")] pub had_errors: bool, #[serde(skip_serializing_if = "Option::is_none")] pub outputs: Option>, @@ -162,6 +170,7 @@ impl ConfigurationSetResult { #[must_use] pub fn new() -> Self { Self { + execution_information: None, metadata: None, results: Vec::new(), messages: Vec::new(), @@ -181,6 +190,8 @@ impl Default for ConfigurationSetResult { #[serde(deny_unknown_fields)] #[dsc_repo_schema(base_name = "test.full", folder_path = "outputs/resource")] pub struct ResourceTestResult { + #[serde(rename = "executionInformation", skip_serializing_if = "Option::is_none")] + pub execution_information: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, pub name: String, @@ -211,13 +222,13 @@ impl Default for GroupResourceTestResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema(base_name = "test", folder_path = "outputs/config")] pub struct ConfigurationTestResult { + pub execution_information: Option, pub metadata: Option, pub results: Vec, pub messages: Vec, - #[serde(rename = "hadErrors")] pub had_errors: bool, #[serde(skip_serializing_if = "Option::is_none")] pub outputs: Option>, @@ -227,6 +238,7 @@ impl ConfigurationTestResult { #[must_use] pub fn new() -> Self { Self { + execution_information: None, metadata: None, results: Vec::new(), messages: Vec::new(), @@ -243,13 +255,13 @@ impl Default for ConfigurationTestResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema(base_name = "export", folder_path = "outputs/config")] pub struct ConfigurationExportResult { + pub execution_information: Option, pub metadata: Option, pub result: Option, pub messages: Vec, - #[serde(rename = "hadErrors")] pub had_errors: bool, #[serde(skip_serializing_if = "Option::is_none")] pub outputs: Option>, @@ -259,6 +271,7 @@ impl ConfigurationExportResult { #[must_use] pub fn new() -> Self { Self { + execution_information: None, metadata: None, result: None, messages: Vec::new(), diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 47816ec06..98d5a7f40 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::configure::config_doc::{ExecutionInformation, ResourceDirective}; use crate::configure::context::{Context, ProcessMode}; use crate::configure::parameters::import_parameters; use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, ResourceDiscoveryMode, RestartRequired, ValueOrCopy}}; @@ -15,6 +16,7 @@ use crate::DscResource; use crate::discovery::Discovery; use crate::parser::Statement; use crate::progress::{Failure, ProgressBar, ProgressFormat}; +use crate::types::{SemanticVersion, SemanticVersionReq}; use crate::util::resource_id; use self::config_doc::{Configuration, DataType, MicrosoftDscMetadata, Operation, SecurityContextKind}; use self::depends_on::get_resource_invocation_order; @@ -103,13 +105,15 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf } r.properties = escape_property_values(&props)?; let mut properties = serde_json::to_value(&r.properties)?; - get_metadata_from_result(None, &mut properties, &mut metadata)?; + let mut execution_information = ExecutionInformation::new(); + get_metadata_from_result(None, &mut properties, &mut metadata, &mut execution_information)?; r.properties = Some(properties.as_object().cloned().unwrap_or_default()); r.metadata = if metadata.microsoft.is_some() || !metadata.other.is_empty() { Some(metadata) } else { None }; + r.execution_information = Some(execution_information); conf.resources.push(r); } @@ -222,48 +226,57 @@ fn add_metadata(dsc_resource: &DscResource, mut properties: Option) -> Option { - if let Some(resource_metadata) = resource_metadata { - if let Some(microsoft_metadata) = &resource_metadata.microsoft { - if let Some(require_adapter) = µsoft_metadata.require_adapter { - return Some(require_adapter.clone()); - } +fn get_require_adapter_from_directive(resource_directives: &Option) -> Option { + if let Some(directives) = resource_directives { + if let Some(require_adapter) = &directives.require_adapter { + return Some(require_adapter.clone()); } } None } -fn check_security_context(metadata: Option<&Metadata>) -> Result<(), DscError> { - if metadata.is_none() { +fn check_security_context(metadata: Option<&Metadata>, directive_security_context: Option<&SecurityContextKind>) -> Result<(), DscError> { + if metadata.is_none() && directive_security_context.is_none() { return Ok(()); } + let mut security_context_required: Option<&SecurityContextKind> = None; if let Some(metadata) = &metadata { if let Some(microsoft_dsc) = &metadata.microsoft { if let Some(required_security_context) = µsoft_dsc.security_context { - match required_security_context { - SecurityContextKind::Current => { - // no check needed - }, - SecurityContextKind::Elevated => { - if get_security_context() != SecurityContext::Admin { - return Err(DscError::SecurityContext(t!("configure.mod.elevationRequired").to_string())); - } - }, - SecurityContextKind::Restricted => { - if get_security_context() != SecurityContext::User { - return Err(DscError::SecurityContext(t!("configure.mod.restrictedRequired").to_string())); - } - }, - } + warn!("{}", t!("configure.mod.securityContextInMetadataDeprecated")); + security_context_required = Some(required_security_context); } } } + if let Some(directive_security_context) = &directive_security_context { + if security_context_required.is_some() && security_context_required != Some(directive_security_context) { + return Err(DscError::SecurityContext(t!("configure.mod.conflictingSecurityContext", metadata = security_context_required.unwrap(), directive = directive_security_context).to_string())); + } + security_context_required = Some(directive_security_context); + } + + match security_context_required { + Some(SecurityContextKind::Elevated) => { + if get_security_context() != SecurityContext::Admin { + return Err(DscError::SecurityContext(t!("configure.mod.elevationRequired").to_string())); + } + }, + Some(SecurityContextKind::Restricted) => { + if get_security_context() != SecurityContext::User { + return Err(DscError::SecurityContext(t!("configure.mod.restrictedRequired").to_string())); + } + }, + None | Some(SecurityContextKind::Current) => { + // no check needed + }, + } + Ok(()) } -fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Value, metadata: &mut Metadata) -> Result<(), DscError> { +fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Value, metadata: &mut Metadata, execution_information: &mut ExecutionInformation) -> Result<(), DscError> { if let Some(metadata_value) = result.get("_metadata") { if let Some(metadata_map) = metadata_value.as_object() { for (key, value) in metadata_map { @@ -274,6 +287,7 @@ fn get_metadata_from_result(mut context: Option<&mut Context>, result: &mut Valu if let Some(ref mut context) = context { if key == "_restartRequired" { if let Ok(restart_required) = serde_json::from_value::>(value.clone()) { + execution_information.restart_required = Some(restart_required.clone()); context.restart_required.get_or_insert_with(Vec::new).extend(restart_required); } else { warn!("{}", t!("configure.mod.metadataRestartRequiredInvalid", value = value)); @@ -394,7 +408,9 @@ impl Configurator { progress.write_increment(1); continue; } - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let directive_security_context = resource.directives.as_ref().and_then(|d| d.security_context.as_ref()); + check_security_context(resource.metadata.as_ref(), directive_security_context)?; + let adapter = get_require_adapter_from_directive(&resource.directives); let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; @@ -410,6 +426,7 @@ impl Configurator { }, }; let end_datetime = chrono::Local::now(); + let mut execution_information = ExecutionInformation::new_with_duration(&start_datetime, &end_datetime); let mut metadata = Metadata { microsoft: Some( MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime) @@ -420,7 +437,7 @@ impl Configurator { match &mut get_result { GetResult::Resource(ref mut resource_result) => { self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_result.actual_state)?); - get_metadata_from_result(Some(&mut self.context), &mut resource_result.actual_state, &mut metadata)?; + get_metadata_from_result(Some(&mut self.context), &mut resource_result.actual_state, &mut metadata, &mut execution_information)?; }, GetResult::Group(group) => { let mut results = Vec::::new(); @@ -431,6 +448,7 @@ impl Configurator { }, } let resource_result = config_result::ResourceGetResult { + execution_information: Some(execution_information), metadata: Some(metadata), name: evaluated_name, resource_type: resource.resource_type.clone(), @@ -444,6 +462,9 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Get) ); + let mut execution_information = ExecutionInformation::new(); + self.get_execution_information(Operation::Get, &mut execution_information); + result.execution_information = Some(execution_information); self.process_output()?; if !self.context.outputs.is_empty() { result.outputs = Some(self.context.outputs.clone()); @@ -479,7 +500,9 @@ impl Configurator { progress.write_increment(1); continue; } - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let directive_security_context = resource.directives.as_ref().and_then(|d| d.security_context.as_ref()); + check_security_context(resource.metadata.as_ref(), directive_security_context)?; + let adapter = get_require_adapter_from_directive(&resource.directives); let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; @@ -590,11 +613,13 @@ impl Configurator { } // Process metadata - only add whatIf if we have ResourceWhatIf variant + let mut execution_information = ExecutionInformation::new_with_duration(&start_datetime, &end_datetime); let mut other_metadata = Map::new(); if self.context.execution_type == ExecutionKind::WhatIf { if let Some(delete_res) = delete_what_if_metadata { if let Some(metadata) = delete_res.metadata { if let Some(what_if) = metadata.what_if { + execution_information.what_if = Some(what_if.clone()); other_metadata.insert("whatIf".to_string(), what_if); } } @@ -610,7 +635,7 @@ impl Configurator { match &mut set_result { SetResult::Resource(resource_result) => { self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_result.after_state)?); - get_metadata_from_result(Some(&mut self.context), &mut resource_result.after_state, &mut metadata)?; + get_metadata_from_result(Some(&mut self.context), &mut resource_result.after_state, &mut metadata, &mut execution_information)?; }, SetResult::Group(group) => { let mut results = Vec::::new(); @@ -621,6 +646,7 @@ impl Configurator { }, } let resource_result = config_result::ResourceSetResult { + execution_information: Some(execution_information), metadata: Some(metadata), name: evaluated_name, resource_type: resource.resource_type.clone(), @@ -634,6 +660,9 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Set) ); + let mut execution_information = ExecutionInformation::new(); + self.get_execution_information(Operation::Set, &mut execution_information); + result.execution_information = Some(execution_information); self.process_output()?; if !self.context.outputs.is_empty() { result.outputs = Some(self.context.outputs.clone()); @@ -664,7 +693,9 @@ impl Configurator { progress.write_increment(1); continue; } - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let directive_security_context = resource.directives.as_ref().and_then(|d| d.security_context.as_ref()); + check_security_context(resource.metadata.as_ref(), directive_security_context)?; + let adapter = get_require_adapter_from_directive(&resource.directives); let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; @@ -682,6 +713,7 @@ impl Configurator { }, }; let end_datetime = chrono::Local::now(); + let mut execution_information = ExecutionInformation::new_with_duration(&start_datetime, &end_datetime); let mut metadata = Metadata { microsoft: Some( MicrosoftDscMetadata::new_with_duration(&start_datetime, &end_datetime) @@ -691,7 +723,7 @@ impl Configurator { match &mut test_result { TestResult::Resource(resource_test_result) => { self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_test_result.actual_state)?); - get_metadata_from_result(Some(&mut self.context), &mut resource_test_result.actual_state, &mut metadata)?; + get_metadata_from_result(Some(&mut self.context), &mut resource_test_result.actual_state, &mut metadata, &mut execution_information)?; }, TestResult::Group(group) => { let mut results = Vec::::new(); @@ -702,6 +734,7 @@ impl Configurator { }, } let resource_result = config_result::ResourceTestResult { + execution_information: Some(execution_information), metadata: Some(metadata), name: evaluated_name, resource_type: resource.resource_type.clone(), @@ -715,6 +748,9 @@ impl Configurator { result.metadata = Some( self.get_result_metadata(Operation::Test) ); + let mut execution_information = ExecutionInformation::new(); + self.get_execution_information(Operation::Test, &mut execution_information); + result.execution_information = Some(execution_information); self.process_output()?; if !self.context.outputs.is_empty() { result.outputs = Some(self.context.outputs.clone()); @@ -748,7 +784,9 @@ impl Configurator { progress.write_increment(1); continue; } - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let directive_security_context = resource.directives.as_ref().and_then(|d| d.security_context.as_ref()); + check_security_context(resource.metadata.as_ref(), directive_security_context)?; + let adapter = get_require_adapter_from_directive(&resource.directives); let Some(dsc_resource) = discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; @@ -1009,12 +1047,10 @@ impl Configurator { Metadata { microsoft: Some( MicrosoftDscMetadata { - require_adapter: None, duration: Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string()), end_datetime: Some(end_datetime.to_rfc3339()), execution_type: Some(self.context.execution_type.clone()), operation: Some(operation), - resource_discovery: None, restart_required: self.context.restart_required.clone(), security_context: Some(self.context.security_context.clone()), start_datetime: Some(self.context.start_datetime.to_rfc3339()), @@ -1026,30 +1062,58 @@ impl Configurator { } } + fn get_execution_information(&self, operation: Operation, execution_information: &mut ExecutionInformation) { + let end_datetime = chrono::Local::now(); + execution_information.duration = Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string()); + execution_information.end_datetime = Some(end_datetime.to_rfc3339()); + execution_information.start_datetime = Some(self.context.start_datetime.to_rfc3339()); + execution_information.version = self.context.dsc_version.clone(); + execution_information.execution_type = Some(self.context.execution_type.clone()); + execution_information.operation = Some(operation); + execution_information.restart_required = self.context.restart_required.clone(); + execution_information.security_context = Some(self.context.security_context.clone()); + } + fn validate_config(&mut self) -> Result<(), DscError> { let config: Configuration = serde_json::from_str(self.json.as_str())?; - check_security_context(config.metadata.as_ref())?; - - let mut skip_resource_validation = false; - if let Some(metadata) = &config.metadata { - if let Some(microsoft_metadata) = &metadata.microsoft { - if let Some(mode) = µsoft_metadata.resource_discovery { - if *mode == ResourceDiscoveryMode::DuringDeployment { - debug!("{}", t!("configure.mod.skippingResourceDiscovery")); - skip_resource_validation = true; - self.discovery.refresh_cache = true; - } + let config_security_context = if let Some(directives) = &config.directives { + if let Some(security_context) = &directives.security_context { + Some(security_context.clone()) + } else { + None + } + } else { + None + }; + check_security_context(config.metadata.as_ref(), config_security_context.as_ref())?; + + if let Some(directives) = &config.directives { + if let Some(version) = &directives.version { + let dsc_version = SemanticVersion::parse(env!("CARGO_PKG_VERSION"))?; + let version_req = SemanticVersionReq::parse(&version)?; + if !version_req.matches(&dsc_version) { + return Err(DscError::Validation(t!("configure.mod.versionNotSatisfied", required_version = version, current_version = env!("CARGO_PKG_VERSION")).to_string())); } } } - if !skip_resource_validation { + let mut resource_discovery_mode = ResourceDiscoveryMode::PreDeployment; + if let Some(directives) = &config.directives { + if let Some(resource_discovery_directive) = &directives.resource_discovery { + resource_discovery_mode = resource_discovery_directive.clone(); + } + } + + if resource_discovery_mode == ResourceDiscoveryMode::DuringDeployment { + debug!("{}", t!("configure.mod.skippingResourceDiscovery")); + self.discovery.refresh_cache = true; + } else { // Perform discovery of resources used in config // create an array of DiscoveryFilter using the resource types and api_versions from the config let mut discovery_filter: Vec = Vec::new(); let config_copy = config.clone(); for resource in config_copy.resources { - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let adapter = get_require_adapter_from_directive(&resource.directives); let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()); if !discovery_filter.contains(&filter) { discovery_filter.push(filter); @@ -1069,7 +1133,7 @@ impl Configurator { // now check that each resource in the config was found for resource in config.resources.iter() { - let adapter = get_require_adapter_from_metadata(&resource.metadata); + let adapter = get_require_adapter_from_directive(&resource.directives); let Some(_dsc_resource) = self.discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); };