diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 9c8752722..7d403c5e3 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -1,6 +1,8 @@ ## Copyright (c) Microsoft Corporation. All rights reserved. ## Licensed under the MIT License. +using namespace System.Collections.Specialized + [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -99,19 +101,67 @@ class PSResource { return $retValue } + [OrderedDictionary] ToData([bool]$forTest) { + $data = [OrderedDictionary]::new() + + $data['name'] = $this.name + if (-not [string]::IsNullOrEmpty($this.version)) { + $data['version'] = $this.version + } + $data['scope'] = $this.scope + if (-not [string]::IsNullOrEmpty($this.repositoryName)) { + $data['repositoryName'] = $this.repositoryName + } else { + $data['repositoryName'] = "" + } + $data['preRelease'] = $this.preRelease + $data['_exist'] = $this._exist + + if ($forTest) { + $data['_inDesiredState'] = $this._inDesiredState + } + + if ($null -ne $this._metadata) { + $data['_metadata'] = $this._metadata + } + + return $data + } + + [OrderedDictionary] ToData() { + return $this.ToData($false) + } + [string] ToJson() { - [string[]]$excludeProps = @('_inDesiredState') - if ($null -eq $this._metadata) { $excludeProps += '_metadata' } - $retVal = ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -EnumsAsStrings) - 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 - Write-Trace -message "Serialized JSON: $retVal" -level trace + $retVal = $this.ToData() | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings + $this.WriteSerializationTrace($retVal) return $retVal } [string] ToJsonForTest() { - [string[]]$excludeProps = @() - if ($null -eq $this._metadata) { $excludeProps += '_metadata' } - return ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings) + $retVal = $this.ToData($true) | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings + $this.WriteSerializationTrace($retVal, $true) + return $retVal + } + + [void] WriteSerializationTrace([string]$json, [bool]$forTest) { + $pairs = @( + "Name: $($this.name)" + "Version: $($this.version)" + "Scope: $($this.scope)" + "RepositoryName: $($this.repositoryName)" + "PreRelease: $($this.preRelease)" + "_exist: $($this._exist)" + ) + if ($forTest) { + $pairs += "_inDesiredState: $($this._inDesiredState)" + } + Write-Trace -message "Serializing PSResource to JSON. $($pairs -join ', ')" -level debug + Write-Trace -message "Serialized JSON: $json" -level trace + } + + [void] WriteSerializationTrace([string]$json) { + $this.WriteSerializationTrace($json, $false) } } @@ -160,44 +210,56 @@ class PSResourceList { return $true } - [string] ToJson() { - ## Assign the array directly so that an empty list serializes as [] rather than null - [object[]]$resourceObjects = @() + [OrderedDictionary] ToData([bool]$forTest) { + $data = [OrderedDictionary]::new() + if (-not [string]::IsNullOrEmpty($this.repositoryName)) { + $data['repositoryName'] = $this.repositoryName + } else { + $data['repositoryName'] = "" + } if ($this.resources) { - $resourceObjects = @($this.resources | ForEach-Object { - [string[]]$excludeProps = @('_inDesiredState') - if ($null -eq $_._metadata) { $excludeProps += '_metadata' } - $_ | Select-Object -ExcludeProperty $excludeProps - }) - } - $retVal = [ordered]@{ - repositoryName = $this.repositoryName - resources = $resourceObjects - } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings - Write-Trace -message "Serializing PSResourceList to JSON. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug - Write-Trace -message "Serialized JSON: $retVal" -level trace + [OrderedDictionary[]] $resourceData = $this.resources | ForEach-Object { $_.ToData($forTest) } + $data['resources'] = $resourceData + } + if ($this.trustedRepository) { + $data['trustedRepository'] = $this.trustedRepository + } + if ($forTest) { + $data['_inDesiredState'] = $this._inDesiredState + } + return $data + } + [OrderedDictionary] ToData() { + return $this.ToData($false) + } + + [string] ToJson() { + $retVal = $this.ToData() | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings + $this.WriteSerializationTrace($retVal) + return $retVal } [string] ToJsonForTest() { - Write-Trace -message "Serializing PSResourceList to JSON for test output. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug - [object[]]$resourceObjects = @() - if ($this.resources) { - $resourceObjects = @($this.resources | ForEach-Object { - [string[]]$excludeProps = @() - if ($null -eq $_._metadata) { $excludeProps += '_metadata' } - if ($excludeProps.Count -gt 0) { $_ | Select-Object -ExcludeProperty $excludeProps } else { $_ } - }) - } - $retVal = [ordered]@{ - repositoryName = $this.repositoryName - resources = $resourceObjects - trustedRepository = $this.trustedRepository - _inDesiredState = $this._inDesiredState - } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings - Write-Trace -message "Serialized JSON: $retVal" -level trace + $retVal = $this.ToData($true) | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings + $this.WriteSerializationTrace($retVal, $true) return $retVal } + + [void] WriteSerializationTrace([string]$json, [bool]$forTest) { + $preamble = 'Serializing PSResourceList to JSON' + $preamble += $forTest ? ' for test output.' : '.' + $pairs = @( + "repositoryName: $($this.repositoryName)" + "trustedRepository: $($this.trustedRepository)" + "resourcesCount: $($this.resources.Count)" + ) + Write-Trace -message "$preamble $($pairs -join ', ')" -level debug + Write-Trace -message "Serialized JSON: $json" -level trace + } + [void] WriteSerializationTrace([string]$json) { + $this.WriteSerializationTrace($json, $false) + } } class Repository { @@ -238,8 +300,31 @@ class Repository { $this.repositoryType = 'Unknown' } + [OrderedDictionary] ToData([bool]$forTest) { + $data = [OrderedDictionary]::new() + $data['name'] = $this.name + if (-not [string]::IsNullOrEmpty($this.uri)) { + $data['uri'] = $this.uri + } + if ($this._exist) { + $data['trusted'] = $this.trusted + $data['priority'] = $this.priority + } + if (-not [string]::IsNullOrEmpty($this.repositoryType)) { + $data['repositoryType'] = $this.repositoryType + } + $data['_exist'] = $this._exist + + + return $data + } + [OrderedDictionary] ToData() { + return $this.ToData($false) + } + [string] ToJson() { - return ($this | ConvertTo-Json -Compress -EnumsAsStrings) + $retVal = $this.ToData() | ConvertTo-Json -Compress -EnumsAsStrings -Depth 5 + return $retVal } } @@ -568,9 +653,12 @@ function ExportOperation { exit [ExitCode]::ExportNotImplemented } 'psresourcelist' { - $currentUserPSResources = Get-PSResource - $allUsersPSResources = Get-PSResource -Scope AllUsers - PopulatePSResourceListObject -allUsersPSResources $allUsersPSResources -currentUserPSResources $currentUserPSResources + $populatingParams = @{ + currentUserPSResources = GetAllPsResourcesInScope -Scope CurrentUser + allUsersPSResources = GetAllPsResourcesInScope -Scope AllUsers + } + + PopulatePSResourceListObject @populatingParams } default { Write-Trace -level error -message "Unknown ResourceType: $ResourceType" @@ -781,6 +869,9 @@ function SetOperation { if ($property -eq 'repositoryType') { $splatt['ApiVersion'] = $inputObj.$property } + elseif ($property -eq 'uri' -and $inputObj._exist -and $inputObj.name -eq 'PSGallery') { + continue + } else { $splatt[$property] = $inputObj.$property } @@ -861,6 +952,22 @@ function DeleteOperation { } } +function GetAllPsResourcesInScope { + param( + [Scope]$Scope + ) + + try { + Get-PSResource -Scope $Scope -ErrorAction Stop + } catch [Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceNotFoundException] { + # Everything is fine, there's just no installed resources + @() + } catch { + Write-Trace -level error -message "Failed to get PSResources for '$Scope' scope: $_" + @() + } +} + function PopulatePSResourceListObjectByRepository { param ( $resourcesExist, @@ -915,24 +1022,32 @@ function PopulatePSResourceListObject { $allPSResources = @() - $allPSResources += $allUsersPSResources | ForEach-Object { - return [PSResource]::new( - $_.Name, - $_.Version, - [Scope]"AllUsers", - $_.Repository, - $_.PreRelease ? $true : $false - ) + if (@($allUsersPSResources).count -gt 0) { + $allPSResources += $allUsersPSResources | ForEach-Object { + return [PSResource]::new( + $_.Name, + $_.Version, + [Scope]"AllUsers", + $_.Repository, + $_.PreRelease ? $true : $false + ) + } + } else { + Write-Trace -level info "No PSResources found for AllUsers scope." } - $allPSResources += $currentUserPSResources | ForEach-Object { - return [PSResource]::new( - $_.Name, - $_.Version, - [Scope]"CurrentUser", - $_.Repository, - $_.PreRelease ? $true : $false - ) + if (@($currentUserPSResources).count -gt 0) { + $allPSResources += $currentUserPSResources | ForEach-Object { + return [PSResource]::new( + $_.Name, + $_.Version, + [Scope]"CurrentUser", + $_.Repository, + $_.PreRelease ? $true : $false + ) + } + } else { + Write-Trace -level info "No PSResources found for CurrentUser scope." } $repoGrps = $allPSResources | Group-Object -Property repositoryName diff --git a/src/dsc/psresourcelist.dsc.resource.json b/src/dsc/psresourcelist.dsc.resource.json index 6681c9d72..99ccce8de 100644 --- a/src/dsc/psresourcelist.dsc.resource.json +++ b/src/dsc/psresourcelist.dsc.resource.json @@ -106,7 +106,7 @@ "repositoryName": { "title": "Repository Name", "description": "The name of the repository from where the resources are acquired.", - "type": ["string", "null"] + "type": "string" }, "trustedRepository": { "title": "Trusted Repository", @@ -129,7 +129,7 @@ }, "$defs": { "Scope": { - "type": ["string", "null"], + "type": "string", "title": "Scope", "description": "Scope of the resource installation.", "enum": [ @@ -147,12 +147,12 @@ "name": { "title": "Name", "description": "The name of the resource.", - "type": ["string", "null"] + "type": "string" }, "version": { "title": "Version", "description": "The version range of the resource.", - "type": ["string", "null"] + "type": "string" }, "scope": { "title": "Scope", @@ -162,7 +162,7 @@ "repositoryName": { "title": "Repository Name", "description": "The name of the repository from where the resource is acquired.", - "type": ["string", "null"] + "type": "string" }, "preRelease": { "title": "Pre-Release version", diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index d5a7c4218..12c1c9246 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -74,25 +74,23 @@ "description": "A PowerShell Resource repository from where to acquire the resources.", "type": "object", "additionalProperties": false, + "required": ["name"], "allOf": [ { + "$comment": "If the repository name is not `PSGallery` (case-insensitive) and `_exist` is `true` or undefined, then the 'uri' property is required.", "if": { - "properties": { - "_exist": { - "const": false + "not": { + "properties": { + "name": { "pattern": "(?i)^PSGallery$" } } + }, + "properties": { + "_exist": { "const": true } } }, "then": { - "required": [ - "name" - ] - }, - "else": { - "required": [ - "name", - "uri" - ] + "required": ["uri"], + "errorMessage": "The `uri` property is required when `name` is not `PSGallery` (case-insensitive) and `_exist` is not defined as `false`" } } ], @@ -105,7 +103,7 @@ "uri": { "title": "URI", "description": "The URI of the repository.", - "type": ["string", "null"], + "type": "string", "format": "uri" }, "trusted": { diff --git a/test/DscResource/PSResourceGetDSCResource.Tests.ps1 b/test/DscResource/PSResourceGetDSCResource.Tests.ps1 index fafc91006..974cf6282 100644 --- a/test/DscResource/PSResourceGetDSCResource.Tests.ps1 +++ b/test/DscResource/PSResourceGetDSCResource.Tests.ps1 @@ -56,7 +56,11 @@ Describe "DSC resource schema tests" -tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -105,7 +109,11 @@ Describe 'Repository Resource Tests' -Tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -207,7 +215,11 @@ Describe "PSResourceList Resource Tests" -Tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -451,7 +463,11 @@ Describe 'E2E tests for Repository resource' -Tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -512,7 +528,11 @@ Describe 'E2E tests for PSResourceList resource' -Tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -523,7 +543,11 @@ Describe 'E2E tests for PSResourceList resource' -Tags 'CI' { ## Therefore, skipping these tests on Windows PowerShell is necessary because the modules installed by DSC will not be visible in that environment. $isOnWindowsPowerShell = $PSVersionTable.PSVersion.Major -lt 6 - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $isOnWindowsPowerShell } @@ -636,7 +660,11 @@ Describe "Error code tests" -Tags 'CI' { $skipTest = $null -eq (Get-Command -Type Application -Name pwsh -ErrorAction SilentlyContinue) if ($skipTest) { - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $skipTest return } @@ -677,7 +705,11 @@ Describe 'PSResourceList what-if tests' -Tags 'CI' { SetupTestRepos $isOnWindowsPowerShell = $PSVersionTable.PSVersion.Major -lt 6 - $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $originalDefaultParameterValues = if ($null -ne $PSDefaultParameterValues) { + $PSDefaultParameterValues.Clone() + } else { + @{} + } $PSDefaultParameterValues['it:skip'] = $isOnWindowsPowerShell }