Skip to content

Commit f3e064c

Browse files
Gijsreynadityapatwardhan
authored andcommitted
Add 'WhatIf' functionality to PSResourceList operations
1 parent 74849e1 commit f3e064c

3 files changed

Lines changed: 190 additions & 17 deletions

File tree

‎src/dsc/psresourceget.ps1‎

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ param(
1010
[ValidateSet('get', 'set', 'test', 'delete', 'export')]
1111
[string]$Operation,
1212
[Parameter(ValueFromPipeline)]
13-
$stdinput
13+
$stdinput,
14+
[switch]$WhatIf
1415
)
1516

1617
enum Scope {
@@ -42,6 +43,7 @@ class PSResource {
4243
[bool]$preRelease
4344
[bool]$_exist
4445
[bool]$_inDesiredState
46+
[object]$_metadata
4547

4648
PSResource([string]$name, [string]$version, [Scope]$scope, [string]$repositoryName, [bool]$preRelease) {
4749
$this.name = $name
@@ -98,14 +100,18 @@ class PSResource {
98100
}
99101

100102
[string] ToJson() {
101-
$retVal = ($this | Select-Object -ExcludeProperty _inDesiredState | ConvertTo-Json -Compress -EnumsAsStrings)
103+
[string[]]$excludeProps = @('_inDesiredState')
104+
if ($null -eq $this._metadata) { $excludeProps += '_metadata' }
105+
$retVal = ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -EnumsAsStrings)
102106
Write-Trace -message "Serializing PSResource to JSON. Name: $($this.name), Version: $($this.version), Scope: $($this.scope), RepositoryName: $($this.repositoryName), PreRelease: $($this.preRelease), _exist: $($this._exist)" -level debug
103107
Write-Trace -message "Serialized JSON: $retVal" -level trace
104108
return $retVal
105109
}
106110

107111
[string] ToJsonForTest() {
108-
return ($this | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings)
112+
[string[]]$excludeProps = @()
113+
if ($null -eq $this._metadata) { $excludeProps += '_metadata' }
114+
return ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings)
109115
}
110116
}
111117

@@ -155,23 +161,29 @@ class PSResourceList {
155161
}
156162

157163
[string] ToJson() {
158-
$resourceJson = if ($this.resources) { ($this.resources | ForEach-Object { $_.ToJson() }) -join ',' } else { '' }
159-
$resourceJson = "[$resourceJson]"
160-
$jsonString = "{'repositoryName': '$($this.repositoryName)','resources': $resourceJson}"
161-
$jsonString = $jsonString -replace "'", '"'
162-
$retVal = $jsonString | ConvertFrom-Json | ConvertTo-Json -Compress -EnumsAsStrings
163-
164+
$resourceJsonItems = if ($this.resources) {
165+
@($this.resources | ForEach-Object { $_.ToJson() })
166+
} else { @() }
167+
$resourcesArrayJson = '[' + ($resourceJsonItems -join ',') + ']'
168+
$repoNameJson = $this.repositoryName | ConvertTo-Json -Compress
169+
$retVal = "{`"repositoryName`":$repoNameJson,`"resources`":$resourcesArrayJson}"
164170
Write-Trace -message "Serializing PSResourceList to JSON. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug
165171
Write-Trace -message "Serialized JSON: $retVal" -level trace
166-
167172
return $retVal
168173
}
169174

170175
[string] ToJsonForTest() {
171176
Write-Trace -message "Serializing PSResourceList to JSON for test output. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug
172-
$jsonForTest = $this | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings
173-
Write-Trace -message "Serialized JSON: $jsonForTest" -level trace
174-
return $jsonForTest
177+
$resourceJsonItems = if ($this.resources) {
178+
@($this.resources | ForEach-Object { $_.ToJsonForTest() })
179+
} else { @() }
180+
$resourcesArrayJson = '[' + ($resourceJsonItems -join ',') + ']'
181+
$repoNameJson = $this.repositoryName | ConvertTo-Json -Compress
182+
$trustedStr = ($this.trustedRepository).ToString().ToLower()
183+
$inDesiredStr = ($this._inDesiredState).ToString().ToLower()
184+
$retVal = "{`"repositoryName`":$repoNameJson,`"resources`":$resourcesArrayJson,`"trustedRepository`":$trustedStr,`"_inDesiredState`":$inDesiredStr}"
185+
Write-Trace -message "Serialized JSON: $retVal" -level trace
186+
return $retVal
175187
}
176188
}
177189

@@ -548,11 +560,61 @@ function ExportOperation {
548560
}
549561
}
550562

551-
function SetPSResourceList {
563+
function WhatIfPSResourceList {
552564
param(
553565
$inputObj
554566
)
555567

568+
$repositoryName = $inputObj.repositoryName
569+
$currentState = GetPSResourceList -inputObj $inputObj
570+
$projectedResources = @()
571+
$inputObj.resources | ForEach-Object {
572+
$resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName
573+
$name = $resourceDesiredState.name
574+
$version = $resourceDesiredState.version
575+
$scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { [Scope]'CurrentUser' }
576+
$currentResource = $currentState.resources | Where-Object { $_.name -eq $name } | Select-Object -First 1
577+
578+
if (-not $resourceDesiredState._exist -and $null -ne $currentResource -and $currentResource._exist) {
579+
$msg = "Would uninstall resource '$name'"
580+
Write-Trace -message "WhatIf: $msg." -level debug
581+
$resource = [PSResource]::new($name)
582+
$resource._metadata = [pscustomobject]@{ whatIf = @($msg) }
583+
$projectedResources += $resource
584+
}
585+
elseif ($resourceDesiredState._exist -and ($null -eq $currentResource -or -not $currentResource._exist)) {
586+
$versionStr = if ($version) { $version } else { 'latest' }
587+
$msg = "Would install resource '$name' version '$versionStr'"
588+
Write-Trace -message "WhatIf: $msg." -level debug
589+
$resource = [PSResource]::new($name, $versionStr, [Scope]$scope, $repositoryName, $resourceDesiredState.preRelease)
590+
$resource._metadata = [pscustomobject]@{ whatIf = @($msg) }
591+
$projectedResources += $resource
592+
}
593+
else {
594+
Write-Trace -message "WhatIf: Resource '$name' is already in desired state." -level debug
595+
if ($null -ne $currentResource) {
596+
$projectedResources += $currentResource
597+
}
598+
else {
599+
$projectedResources += $resourceDesiredState
600+
}
601+
}
602+
}
603+
604+
$list = [PSResourceList]::new($repositoryName, $projectedResources, $currentState.trustedRepository)
605+
$list.ToJson()
606+
}
607+
608+
function SetPSResourceList {
609+
param(
610+
$inputObj,
611+
[switch]$WhatIf
612+
)
613+
614+
if ($WhatIf) {
615+
return WhatIfPSResourceList -inputObj $inputObj
616+
}
617+
556618
$repositoryName = $inputObj.repositoryName
557619
$resourcesToUninstall = @()
558620
$resourcesToInstall = [System.Collections.Generic.Dictionary[string, psobject]]::new()
@@ -707,7 +769,7 @@ function SetOperation {
707769
Write-Trace -level error -message "Set operation is not implemented for PSResource resource."
708770
exit [ExitCode]::SetNotImplemented
709771
}
710-
'psresourcelist' { return SetPSResourceList -inputObj $inputObj }
772+
'psresourcelist' { return SetPSResourceList -inputObj $inputObj -WhatIf:$WhatIf }
711773
default {
712774
Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
713775
exit [ExitCode]::UnknownResourceType

‎src/dsc/psresourcelist.dsc.resource.json‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@
3232
"-ExecutionPolicy",
3333
"Bypass",
3434
"-Command",
35-
"$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set; exit $LASTEXITCODE"
35+
"$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set",
36+
{ "whatIfArg": "-WhatIf" }
3637
],
3738
"input": "stdin",
38-
"return": "stateAndDiff"
39+
"return": "stateAndDiff",
40+
"whatIfReturns": "state"
3941
},
4042
"export": {
4143
"executable": "pwsh",
@@ -161,6 +163,20 @@
161163
},
162164
"_inDesiredState": {
163165
"$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json"
166+
},
167+
"_metadata": {
168+
"type": "object",
169+
"title": "Metadata",
170+
"description": "Metadata populated during what-if operations.",
171+
"readOnly": true,
172+
"additionalProperties": false,
173+
"properties": {
174+
"whatIf": {
175+
"type": "array",
176+
"description": "Messages describing what the set operation would do.",
177+
"items": { "type": "string" }
178+
}
179+
}
164180
}
165181
}
166182
},

‎test/DscResource/PSResourceGetDSCResource.Tests.ps1‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,3 +670,98 @@ Describe "Error code tests" -Tags 'CI' {
670670
$out[-1] | Should -BeLike '*Could not install one or more resources (during set operation)*'
671671
}
672672
}
673+
674+
Describe 'PSResourceList what-if tests' -Tags 'CI' {
675+
BeforeAll {
676+
SetupDsc
677+
SetupTestRepos
678+
679+
$isOnWindowsPowerShell = $PSVersionTable.PSVersion.Major -lt 6
680+
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
681+
$PSDefaultParameterValues['it:skip'] = $isOnWindowsPowerShell
682+
}
683+
684+
AfterAll {
685+
$global:PSDefaultParameterValues = $originalDefaultParameterValues
686+
Get-RevertPSResourceRepositoryFile
687+
}
688+
689+
It 'What-if install does not modify the system' {
690+
Uninstall-PSResource -Name $script:testModuleName -ErrorAction SilentlyContinue
691+
Uninstall-PSResource -Name $script:testModuleName2 -ErrorAction SilentlyContinue
692+
693+
$config_yaml = @"
694+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
695+
resources:
696+
- name: PSResourceList what-if install
697+
type: Microsoft.PowerShell.PSResourceGet/PSResourceList
698+
properties:
699+
repositoryName: $script:localRepo
700+
trustedRepository: true
701+
resources:
702+
- name: $script:testModuleName
703+
version: '5.0.0'
704+
- name: $script:testModuleName2
705+
version: '5.0.0'
706+
"@
707+
708+
$out = & $script:dscExe config set --what-if --input $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
709+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw)
710+
711+
$result = $out.results.result[0].afterState
712+
$result.repositoryName | Should -BeExactly $script:localRepo
713+
$result.resources.Count | Should -Be 2
714+
$result.resources[0]._metadata.whatIf[0] | Should -Match 'Would install'
715+
$result.resources[1]._metadata.whatIf[0] | Should -Match 'Would install'
716+
717+
Get-PSResource -Name $script:testModuleName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
718+
Get-PSResource -Name $script:testModuleName2 -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
719+
}
720+
721+
It 'What-if uninstall does not modify the system' {
722+
Install-PSResource -Name $script:testModuleName -Version '5.0.0' -Repository $script:localRepo -TrustRepository -Reinstall
723+
724+
$config_yaml = @"
725+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
726+
resources:
727+
- name: PSResourceList what-if uninstall
728+
type: Microsoft.PowerShell.PSResourceGet/PSResourceList
729+
properties:
730+
repositoryName: $script:localRepo
731+
resources:
732+
- name: $script:testModuleName
733+
_exist: false
734+
"@
735+
736+
$out = & $script:dscExe config set --what-if --input $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
737+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw)
738+
739+
$result = $out.results.result[0].afterState
740+
$result.resources[0]._exist | Should -BeFalse
741+
$result.resources[0]._metadata.whatIf[0] | Should -Match 'Would uninstall'
742+
743+
Get-PSResource -Name $script:testModuleName -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
744+
}
745+
746+
It 'What-if returns no metadata for resources already in desired state' {
747+
Install-PSResource -Name $script:testModuleName -Version '5.0.0' -Repository $script:localRepo -TrustRepository -Reinstall
748+
749+
$config_yaml = @"
750+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
751+
resources:
752+
- name: PSResourceList what-if in desired state
753+
type: Microsoft.PowerShell.PSResourceGet/PSResourceList
754+
properties:
755+
repositoryName: $script:localRepo
756+
resources:
757+
- name: $script:testModuleName
758+
version: '5.0.0'
759+
"@
760+
761+
$out = & $script:dscExe config set --what-if --input $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
762+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw)
763+
764+
$result = $out.results.result[0].afterState
765+
$result.resources[0]._metadata | Should -BeNullOrEmpty
766+
}
767+
}

0 commit comments

Comments
 (0)