|
10 | 10 | [ValidateSet('get', 'set', 'test', 'delete', 'export')] |
11 | 11 | [string]$Operation, |
12 | 12 | [Parameter(ValueFromPipeline)] |
13 | | - $stdinput |
| 13 | + $stdinput, |
| 14 | + [switch]$WhatIf |
14 | 15 | ) |
15 | 16 |
|
16 | 17 | enum Scope { |
@@ -42,6 +43,7 @@ class PSResource { |
42 | 43 | [bool]$preRelease |
43 | 44 | [bool]$_exist |
44 | 45 | [bool]$_inDesiredState |
| 46 | + [object]$_metadata |
45 | 47 |
|
46 | 48 | PSResource([string]$name, [string]$version, [Scope]$scope, [string]$repositoryName, [bool]$preRelease) { |
47 | 49 | $this.name = $name |
@@ -98,14 +100,18 @@ class PSResource { |
98 | 100 | } |
99 | 101 |
|
100 | 102 | [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) |
102 | 106 | 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 |
103 | 107 | Write-Trace -message "Serialized JSON: $retVal" -level trace |
104 | 108 | return $retVal |
105 | 109 | } |
106 | 110 |
|
107 | 111 | [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) |
109 | 115 | } |
110 | 116 | } |
111 | 117 |
|
@@ -155,23 +161,42 @@ class PSResourceList { |
155 | 161 | } |
156 | 162 |
|
157 | 163 | [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 | + ## Assign the array directly so that an empty list serializes as [] rather than null |
| 165 | + [object[]]$resourceObjects = @() |
| 166 | + if ($this.resources) { |
| 167 | + $resourceObjects = @($this.resources | ForEach-Object { |
| 168 | + [string[]]$excludeProps = @('_inDesiredState') |
| 169 | + if ($null -eq $_._metadata) { $excludeProps += '_metadata' } |
| 170 | + $_ | Select-Object -ExcludeProperty $excludeProps |
| 171 | + }) |
| 172 | + } |
| 173 | + $retVal = [ordered]@{ |
| 174 | + repositoryName = $this.repositoryName |
| 175 | + resources = $resourceObjects |
| 176 | + } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings |
164 | 177 | Write-Trace -message "Serializing PSResourceList to JSON. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug |
165 | 178 | Write-Trace -message "Serialized JSON: $retVal" -level trace |
166 | | - |
167 | 179 | return $retVal |
168 | 180 | } |
169 | 181 |
|
170 | 182 | [string] ToJsonForTest() { |
171 | 183 | 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 |
| 184 | + [object[]]$resourceObjects = @() |
| 185 | + if ($this.resources) { |
| 186 | + $resourceObjects = @($this.resources | ForEach-Object { |
| 187 | + [string[]]$excludeProps = @() |
| 188 | + if ($null -eq $_._metadata) { $excludeProps += '_metadata' } |
| 189 | + if ($excludeProps.Count -gt 0) { $_ | Select-Object -ExcludeProperty $excludeProps } else { $_ } |
| 190 | + }) |
| 191 | + } |
| 192 | + $retVal = [ordered]@{ |
| 193 | + repositoryName = $this.repositoryName |
| 194 | + resources = $resourceObjects |
| 195 | + trustedRepository = $this.trustedRepository |
| 196 | + _inDesiredState = $this._inDesiredState |
| 197 | + } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings |
| 198 | + Write-Trace -message "Serialized JSON: $retVal" -level trace |
| 199 | + return $retVal |
175 | 200 | } |
176 | 201 | } |
177 | 202 |
|
@@ -353,21 +378,27 @@ function GetPSResourceList { |
353 | 378 | $preferred = $matchingResources | Where-Object { |
354 | 379 | try { SatisfiesVersion -version $_.Version -versionRange $inputResource.Version } catch { $false } |
355 | 380 | } | Select-Object -First 1 |
356 | | - } else { |
| 381 | + } |
| 382 | + elseif (-not ($resolvedResources | Where-Object { $_.Name -eq $inputResource.Name })) { |
| 383 | + # No version constraint: any installed version means the resource exists. |
| 384 | + # Only record the first match so that one input resource maps to one current resource. |
| 385 | + Write-Trace -message "No version constraint for input: $($inputResource.Name). Treating installed version $($matchingResources[0].Version) as a match." -level debug |
357 | 386 | $preferred = $matchingResources | Select-Object -First 1 |
358 | 387 | } |
359 | 388 |
|
360 | 389 | if ($preferred) { |
361 | 390 | Write-Trace -message "Resource '$($inputResource.Name)' version '$($preferred.Version)' satisfies requested range '$($inputResource.Version)'." -level debug |
362 | 391 | $resolvedResources += $preferred |
363 | | - } else { |
| 392 | + } |
| 393 | + else { |
364 | 394 | # Installed but doesn't satisfy the version range - report actual installed version with _exist = false |
365 | 395 | $fallback = $matchingResources | Select-Object -First 1 |
366 | 396 | Write-Trace -message "Resource '$($inputResource.Name)' installed at '$($fallback.Version)' does not satisfy requested range '$($inputResource.Version)'. Reporting _exist = false." -level debug |
367 | 397 | $fallback._exist = $false |
368 | 398 | $resolvedResources += $fallback |
369 | 399 | } |
370 | | - } else { |
| 400 | + } |
| 401 | + else { |
371 | 402 | Write-Trace -message "Resource '$($inputResource.Name)' is not installed. Reporting _exist = false." -level debug |
372 | 403 | $resolvedResources += [PSResource]::new($inputResource.Name) |
373 | 404 | } |
@@ -548,11 +579,84 @@ function ExportOperation { |
548 | 579 | } |
549 | 580 | } |
550 | 581 |
|
551 | | -function SetPSResourceList { |
| 582 | +function WhatIfPSResourceList { |
552 | 583 | param( |
553 | 584 | $inputObj |
554 | 585 | ) |
555 | 586 |
|
| 587 | + $repositoryName = $inputObj.repositoryName |
| 588 | + $currentState = GetPSResourceList -inputObj $inputObj |
| 589 | + $projectedResources = @() |
| 590 | + $inputObj.resources | ForEach-Object { |
| 591 | + $resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName |
| 592 | + $name = $resourceDesiredState.name |
| 593 | + $version = $resourceDesiredState.version |
| 594 | + $scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { [Scope]'CurrentUser' } |
| 595 | + $currentResource = $currentState.resources | Where-Object { $_.name -eq $name } | Select-Object -First 1 |
| 596 | + |
| 597 | + if (-not $resourceDesiredState._exist -and $null -ne $currentResource -and $currentResource._exist) { |
| 598 | + $msg = "Would uninstall resource '$name'" |
| 599 | + Write-Trace -message "WhatIf: $msg." -level debug |
| 600 | + $resource = [PSResource]::new( |
| 601 | + $currentResource.name, |
| 602 | + $currentResource.version, |
| 603 | + $currentResource.scope, |
| 604 | + $currentResource.repositoryName, |
| 605 | + $currentResource.preRelease |
| 606 | + ) |
| 607 | + $resource._exist = $false |
| 608 | + $resource._metadata = [pscustomobject]@{ whatIf = @($msg) } |
| 609 | + $projectedResources += $resource |
| 610 | + } |
| 611 | + elseif ($resourceDesiredState._exist -and ($null -eq $currentResource -or -not $currentResource._exist)) { |
| 612 | + $versionStr = if ($version) { $version } else { 'latest' } |
| 613 | + $msg = "Would install resource '$name' version '$versionStr'" |
| 614 | + Write-Trace -message "WhatIf: $msg." -level debug |
| 615 | + $resource = [PSResource]::new($name, $versionStr, [Scope]$scope, $repositoryName, $resourceDesiredState.preRelease) |
| 616 | + $resource._metadata = [pscustomobject]@{ whatIf = @($msg) } |
| 617 | + $projectedResources += $resource |
| 618 | + } |
| 619 | + else { |
| 620 | + Write-Trace -message "WhatIf: Resource '$name' is already in desired state." -level debug |
| 621 | + if ($null -ne $currentResource) { |
| 622 | + $projectedResources += $currentResource |
| 623 | + } |
| 624 | + else { |
| 625 | + $projectedResources += $resourceDesiredState |
| 626 | + } |
| 627 | + } |
| 628 | + } |
| 629 | + |
| 630 | + ## Report the same failures a real set operation would hit before installing anything |
| 631 | + $installRequired = @($projectedResources | Where-Object { $_._exist -and $null -ne $_._metadata }).Count -gt 0 |
| 632 | + if ($installRequired) { |
| 633 | + $psRepository = Get-PSResourceRepository -Name $repositoryName -ErrorAction SilentlyContinue |
| 634 | + |
| 635 | + if (-not $psRepository) { |
| 636 | + Write-Trace -level error -message "Repository '$repositoryName' not found. Cannot install resources." |
| 637 | + exit [ExitCode]::RepositoryNotFound |
| 638 | + } |
| 639 | + |
| 640 | + if (-not $psRepository.Trusted -and -not $inputObj.trustedRepository) { |
| 641 | + Write-Trace -level error -message "Repository '$repositoryName' is not trusted. Cannot install resources." |
| 642 | + exit [ExitCode]::RepositoryNotTrusted |
| 643 | + } |
| 644 | + } |
| 645 | + |
| 646 | + $list = [PSResourceList]::new($repositoryName, $projectedResources, $currentState.trustedRepository) |
| 647 | + $list.ToJson() |
| 648 | +} |
| 649 | + |
| 650 | +function SetPSResourceList { |
| 651 | + param( |
| 652 | + $inputObj, |
| 653 | + [switch]$WhatIf |
| 654 | + ) |
| 655 | + |
| 656 | + if ($WhatIf) { |
| 657 | + return WhatIfPSResourceList -inputObj $inputObj |
| 658 | + } |
| 659 | + |
556 | 660 | $repositoryName = $inputObj.repositoryName |
557 | 661 | $resourcesToUninstall = @() |
558 | 662 | $resourcesToInstall = [System.Collections.Generic.Dictionary[string, psobject]]::new() |
@@ -707,7 +811,7 @@ function SetOperation { |
707 | 811 | Write-Trace -level error -message "Set operation is not implemented for PSResource resource." |
708 | 812 | exit [ExitCode]::SetNotImplemented |
709 | 813 | } |
710 | | - 'psresourcelist' { return SetPSResourceList -inputObj $inputObj } |
| 814 | + 'psresourcelist' { return SetPSResourceList -inputObj $inputObj -WhatIf:$WhatIf } |
711 | 815 | default { |
712 | 816 | Write-Trace -level error -message "Unknown ResourceType: $ResourceType" |
713 | 817 | exit [ExitCode]::UnknownResourceType |
|
0 commit comments