From 6191b047b8b69efe9f00abb229eb24899244f18c Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 13 Jun 2025 10:39:10 -0700 Subject: [PATCH 01/16] Initial --- src/dsc/psresourceget.ps1 | 30 ++++++++ src/dsc/respository.dsc.resource.json | 105 ++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/dsc/psresourceget.ps1 create mode 100644 src/dsc/respository.dsc.resource.json diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 new file mode 100644 index 000000000..384cd220d --- /dev/null +++ b/src/dsc/psresourceget.ps1 @@ -0,0 +1,30 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet('repository', 'psresource', 'repositories', 'psresources')] + [string]$ResourceType, + [Parameter(Mandatory = $true)] + [ValidateSet('Get', 'Set', 'Export', 'Test')] + [string]$Operation, + [Parameter(ValueFromPipeline)] + $stdinput +) + +# catch any un-caught exception and write it to the error stream +trap { + Write-Trace -Level Error -message $_.Exception.Message + exit 1 +} + +function Write-Trace { + param( + [string]$message, + [string]$level = 'Error' + ) + + $trace = [pscustomobject]@{ + $level.ToLower() = $message + } | ConvertTo-Json -Compress + + $host.ui.WriteErrorLine($trace) +} diff --git a/src/dsc/respository.dsc.resource.json b/src/dsc/respository.dsc.resource.json new file mode 100644 index 000000000..acd3c5bdd --- /dev/null +++ b/src/dsc/respository.dsc.resource.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.PowerShell.Debug/Echo", + "version": "0.0.1", + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./psresourceget.ps1 -resourcetype 'repository' -operation get" + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./psresourceget.ps1 -resourcetype 'repository' -operation set" + ], + "input": "stdin" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./psresourceget.ps1 -resourcetype 'repository' -operation export" + ], + "input": "stdin" + }, + "test": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./psresourceget.ps1 -resourcetype 'repository' -operation test" + ], + "input": "stdin" + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Repository", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": [ + "null", + "string" + ] + }, + "uri": { + "type": [ + "null", + "string" + ] + }, + "trusted": { + "type": "boolean" + }, + "priority": { + "type": "integer" + }, + "repositoryType": { + "$ref": "#/definitions/RepositoryType" + }, + "_exists": { + "type": "boolean" + } + }, + "definitions": { + "RepositoryType": { + "type": "integer", + "description": "", + "x-enumNames": [ + "Unknown", + "V2", + "V3", + "Local", + "NugetServer", + "ContainerRegistry" + ], + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + } + } + } +} \ No newline at end of file From aca1d97c0cae2442fbeaf446453cd38b5ffee460 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Jul 2025 13:16:48 -0700 Subject: [PATCH 02/16] PSresources --- .vscode/settings.json | 3 +- src/dsc/psresourceget.ps1 | 289 +++++++++++++++++- src/dsc/psresources.dsc.resource.json | 117 +++++++ ...urce.json => repository.dsc.resource.json} | 20 +- 4 files changed, 406 insertions(+), 23 deletions(-) create mode 100644 src/dsc/psresources.dsc.resource.json rename src/dsc/{respository.dsc.resource.json => repository.dsc.resource.json} (78%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1dea6f957..501ade606 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "csharp.semanticHighlighting.enabled": true, "dotnet.automaticallyCreateSolutionInWorkspace": false, "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableRoslynAnalyzers": true + "omnisharp.enableRoslynAnalyzers": true, + "sarif-viewer.connectToGithubCodeScanning": "off" } diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 384cd220d..8815f0e8e 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -1,21 +1,17 @@ +using namespace NuGet.Versioning + [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('repository', 'psresource', 'repositories', 'psresources')] [string]$ResourceType, [Parameter(Mandatory = $true)] - [ValidateSet('Get', 'Set', 'Export', 'Test')] + [ValidateSet('get', 'set', 'export', 'test')] [string]$Operation, [Parameter(ValueFromPipeline)] $stdinput ) -# catch any un-caught exception and write it to the error stream -trap { - Write-Trace -Level Error -message $_.Exception.Message - exit 1 -} - function Write-Trace { param( [string]$message, @@ -28,3 +24,282 @@ function Write-Trace { $host.ui.WriteErrorLine($trace) } + +# catch any un-caught exception and write it to the error stream +trap { + Write-Trace -Level Error -message $_.Exception.Message + exit 1 +} + +function GetAllPSResources { + $resources = Get-PSResource + + +} + +function GetOperation { + param( + [string]$ResourceType + ) + + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop + + switch ($ResourceType) { + 'repository' { + $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorVariable err -ErrorAction SilentlyContinue + + if ($err.FullyQualifiedErrorId -eq 'ErrorGettingSpecifiedRepo,Microsoft.PowerShell.PSResourceGet.Cmdlets.GetPSResourceRepository') { + return PopulateRepositoryObject -RepositoryInfo $null + } + + $ret = PopulateRepositoryObject -RepositoryInfo $rep + return $ret + } + + 'repositories' { return 'Get-PSRepository' } + 'psresource' { return 'Get-PSResource' } + 'psresources' { + + $allPSResources = if ($inputObj.scope) { + Get-PSResource -Scope $inputObj.Scope + } else { + Get-PSResource + } + + if ($inputObj.repositoryName) { + $allPSResources = FilterPSResourcesByRepository -allPSResources $allPSResources -repositoryName $inputObj.repositoryName + } + + $resourcesExist = @() + $resourcesMissing = @() + + foreach ($resource in $allPSResources) { + foreach ($inputResource in $inputObj.resources) { + if ($resource.Name -eq $inputResource.Name) { + if ($inputResource.Version) { + # Use the NuGet.Versioning package if available, otherwise do a simple comparison + try { + $versionRange = [NuGet.Versioning.VersionRange]::Parse($inputResource.Version) + $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($resource.Version.ToString()) + if (-not $versionRange.Satisfies($resourceVersion)) { + continue + } + else { + $resourcesExist += $resource + continue + } + } catch { + # Fallback: simple string comparison (not full NuGet range support) + if ($resource.Version.ToString() -ne $inputObj.resources.Version) { + continue + } + } + } + } + + $resourcesMissing += $resource + } + } + + PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -resourcesMissing $resourcesMissing -repositoryName $inputObj.repositoryName -scope $inputObj.Scope + } + default { throw "Unknown ResourceType: $ResourceType" } + } +} + +function ExportOperation { + switch ($ResourceType) { + 'repository' { + $rep = Get-PSResourceRepository -ErrorAction Stop + + $rep | ForEach-Object { + PopulateRepositoryObject -RepositoryInfo $_ + } + } + + 'repositories' { return 'Get-PSRepository' } + 'psresource' { return 'Get-PSResource' } + 'psresources' { + $allPSResources = Get-PSResource + PopulatePSResourcesObject -allPSResources $allPSResources + } + default { throw "Unknown ResourceType: $ResourceType" } + } +} + +function SetOperation { + param( + [string]$ResourceType + ) + + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop + + switch ($ResourceType) { + 'repository' { + $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorAction SilentlyContinue + + $splatt = @{} + + if ($inputObj.Name) { + $splatt['Name'] = $inputObj.Name + } + + if ($inputObj.Uri) { + $splatt['Uri'] = $inputObj.Uri + } + + if ($inputObj.Trusted) { + $splatt['Trusted'] = $inputObj.Trusted + } + + if ($null -ne $inputObj.Priority ) { + $splatt['Priority'] = $inputObj.Priority + } + + if ($inputObj.repositoryType) { + $splatt['ApiVersion'] = $inputObj.repositoryType + } + + if ($null -eq $rep) { + Register-PSResourceRepository @splatt + } + else { + Set-PSResourceRepository @splatt + } + + return GetOperation -ResourceType $ResourceType + } + + 'repositories' { return 'Set-PSRepository' } + 'psresource' { return 'Set-PSResource' } + 'psresources' { return 'Set-PSResource' } + default { throw "Unknown ResourceType: $ResourceType" } + } +} + +function FilterPSResourcesByRepository { + param ( + $allPSResources, + $repositoryName + ) + + if (-not $repositoryName) { + return $allPSResources + } + + $filteredResources = $allPSResources | Where-Object { $_.Repository -eq $repositoryName } + + return $filteredResources +} + +function PopulatePSResourcesObjectByRepository { + param ( + $resourcesExist, + $resourcesMissing, + $repositoryName, + $scope + ) + + $resources = @() + + $resources += $resourcesExist | ForEach-Object { + [pscustomobject]@{ + name = $_.Name + version = $_.Version.ToString() + _exists = $true + } + } + + $resources += $resourcesMissing | ForEach-Object { + [pscustomobject]@{ + name = $_.Name + version = $_.Version.ToString() + _exists = $false + } + } + + $resourcesObj = if ($scope) { + [pscustomobject]@{ + repositoryName = $repositoryName + scope = $scope + resources = $resources + } + } + else { + [pscustomobject]@{ + repositoryName = $repositoryName + resources = $resources + } + } + + return ($resourcesObj | ConvertTo-Json -Compress) +} + +function PopulatePSResourcesObject { + param ( + $allPSResources + ) + + $repoGrps = $allPSResources | Group-Object -Property Repository + + $repoGrps | ForEach-Object { + $repoName = $_.Name + + + $resources = $_.Group | ForEach-Object { + [pscustomobject]@{ + name = $_.Name + version = $_.Version.ToString() + _exists = $true + } + } + + $resourcesObj = [pscustomobject]@{ + repositoryName = $repoName + resources = $resources + } + + $resourcesObj | ConvertTo-Json -Compress + } +} + +function PopulateRepositoryObject { + param( + $RepositoryInfo + ) + + $repository = if (-not $RepositoryInfo) { + Write-Trace -message "RepositoryInfo is null or empty. Returning _exists = false" -Level Information + + $inputJson = $stdinput | ConvertFrom-Json -ErrorAction Stop + + [pscustomobject]@{ + name = $inputJson.Name + uri = $inputJson.Uri + trusted = $inputJson.Trusted + priority = $inputJson.Priority + repositoryType = $inputJson.repositoryType + _exists = $false + } + } + else { + Write-Trace -message "Populating repository object for: $($RepositoryInfo.Name)" -Level Verbose + [pscustomobject]@{ + name = $RepositoryInfo.Name + uri = $RepositoryInfo.Uri + trusted = $RepositoryInfo.Trusted + priority = $RepositoryInfo.Priority + repositoryType = $RepositoryInfo.ApiVersion + _exists = $true + } + } + + return ($repository | ConvertTo-Json -Compress) +} + +switch ($Operation.ToLower()) { + 'get' { return (GetOperation -ResourceType $ResourceType) } + 'set' { return (SetOperation -ResourceType $ResourceType) } + 'export' { return (ExportOperation -ResourceType $ResourceType) } + default { throw "Unknown Operation: $Operation" } +} diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json new file mode 100644 index 000000000..f6c11cc1c --- /dev/null +++ b/src/dsc/psresources.dsc.resource.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "description": "This resource manages PowerShell resources using PSResourceGet.", + "type": "Microsoft.PowerShell.PSResourceGet/PSResources", + "version": "0.0.1", + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./psresourceget.ps1 -resourcetype 'psresources' -operation 'get'" + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./psresourceget.ps1 -resourcetype 'psresources' -operation set" + ], + "input": "stdin" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./psresourceget.ps1 -resourcetype 'psresources' -operation export" + ], + "input": "stdin" + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PSResources", + "type": "object", + "additionalProperties": false, + "properties": { + "repositoryName": { + "type": [ + "null", + "string" + ] + }, + "scope": { + "$ref": "#/definitions/Scope", + "writeOnly": true + }, + "resources": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PSResource" + } + } + }, + "definitions": { + "Scope": { + "type": "integer", + "description": "", + "x-enumNames": [ + "CurrentUser", + "AllUsers" + ], + "enum": [ + 0, + 1 + ] + }, + "PSResource": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": [ + "null", + "string" + ] + }, + "version": { + "type": [ + "null", + "string" + ] + }, + "scope": { + "$ref": "#/definitions/Scope", + "writeOnly": true + }, + "repositoryName": { + "type": [ + "null", + "string" + ] + }, + "preRelease": { + "type": "boolean", + "writeOnly": true + }, + "_exists": { + "type": "boolean" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/dsc/respository.dsc.resource.json b/src/dsc/repository.dsc.resource.json similarity index 78% rename from src/dsc/respository.dsc.resource.json rename to src/dsc/repository.dsc.resource.json index acd3c5bdd..153573a4d 100644 --- a/src/dsc/respository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -1,6 +1,7 @@ { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", - "type": "Microsoft.PowerShell.Debug/Echo", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "description": "This resource manages PowerShell repositories using PSResourceGet.", + "type": "Microsoft.PowerShell.PSResourceGet/Repository", "version": "0.0.1", "get": { "executable": "pwsh", @@ -9,7 +10,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "./psresourceget.ps1 -resourcetype 'repository' -operation get" + "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation 'get'" ], "input": "stdin" }, @@ -20,7 +21,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "./psresourceget.ps1 -resourcetype 'repository' -operation set" + "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation set" ], "input": "stdin" }, @@ -35,17 +36,6 @@ ], "input": "stdin" }, - "test": { - "executable": "pwsh", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-Command", - "./psresourceget.ps1 -resourcetype 'repository' -operation test" - ], - "input": "stdin" - }, "schema": { "embedded": { "$schema": "http://json-schema.org/draft-04/schema#", From 2568cc146bde03bb34be5c4d47b04b671db8ba4c Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Sun, 13 Jul 2025 08:18:21 -0700 Subject: [PATCH 03/16] PSResources get --- doBuild.ps1 | 6 ++++++ src/dsc/psresourceget.ps1 | 26 ++++++++++++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/doBuild.ps1 b/doBuild.ps1 index 15293d99b..9edfca0d9 100644 --- a/doBuild.ps1 +++ b/doBuild.ps1 @@ -47,6 +47,12 @@ function DoBuild Write-Verbose -Verbose -Message "Copying PSResourceRepository.admx to '$BuildOutPath'" Copy-Item -Path "${SrcPath}/PSResourceRepository.admx" -Dest "$BuildOutPath" -Force + Write-Verbose -Verbose -Message "Copying psresourceget.ps1 to '$BuildOutPath'" + Copy-Item -Path "${SrcPath}/dsc/psresourceget.ps1" -Dest "$BuildOutPath" -Force + + Write-Verbose -Verbose -Message "Copying resource manifests to '$BuildOutPath'" + Copy-Item -Path "${SrcPath}/dsc/*.resource.json" -Dest "$BuildOutPath" -Force + # Build and place binaries if ( Test-Path "${SrcPath}/code" ) { Write-Verbose -Verbose -Message "Building assembly and copying to '$BuildOutPath'" diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 8815f0e8e..412ec8edf 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -1,4 +1,5 @@ -using namespace NuGet.Versioning +## Copyright (c) Microsoft Corporation. All rights reserved. +## Licensed under the MIT License. [CmdletBinding()] param( @@ -33,8 +34,6 @@ trap { function GetAllPSResources { $resources = Get-PSResource - - } function GetOperation { @@ -73,6 +72,8 @@ function GetOperation { $resourcesExist = @() $resourcesMissing = @() + Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" + foreach ($resource in $allPSResources) { foreach ($inputResource in $inputObj.resources) { if ($resource.Name -eq $inputResource.Name) { @@ -81,23 +82,24 @@ function GetOperation { try { $versionRange = [NuGet.Versioning.VersionRange]::Parse($inputResource.Version) $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($resource.Version.ToString()) - if (-not $versionRange.Satisfies($resourceVersion)) { - continue + if ($versionRange.Satisfies($resourceVersion)) { + $resourcesExist += $resource } else { - $resourcesExist += $resource - continue + $resourcesMissing += $inputResource } - } catch { + } + catch { # Fallback: simple string comparison (not full NuGet range support) - if ($resource.Version.ToString() -ne $inputObj.resources.Version) { - continue + if ($resource.Version.ToString() -eq $inputResource.Version) { + $resourcesExist += $resource + } + else { + $resourcesMissing += $inputResource } } } } - - $resourcesMissing += $resource } } From bf192e293484942b027197631902df3fd091172d Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 21 Jul 2025 18:55:26 -0700 Subject: [PATCH 04/16] Fix get for psresources --- src/dsc/psresourceget.ps1 | 59 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 412ec8edf..b609dec98 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -70,7 +70,6 @@ function GetOperation { } $resourcesExist = @() - $resourcesMissing = @() Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" @@ -85,25 +84,19 @@ function GetOperation { if ($versionRange.Satisfies($resourceVersion)) { $resourcesExist += $resource } - else { - $resourcesMissing += $inputResource - } } catch { # Fallback: simple string comparison (not full NuGet range support) if ($resource.Version.ToString() -eq $inputResource.Version) { $resourcesExist += $resource } - else { - $resourcesMissing += $inputResource - } } } } } } - PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -resourcesMissing $resourcesMissing -repositoryName $inputObj.repositoryName -scope $inputObj.Scope + PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -inputResources $inputObj.resources -repositoryName $inputObj.repositoryName -scope $inputObj.Scope } default { throw "Unknown ResourceType: $ResourceType" } } @@ -197,40 +190,44 @@ function FilterPSResourcesByRepository { function PopulatePSResourcesObjectByRepository { param ( $resourcesExist, - $resourcesMissing, + $inputResources, $repositoryName, $scope ) $resources = @() + $resourcesObj = @() - $resources += $resourcesExist | ForEach-Object { - [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exists = $true + if (-not $resourcesExist) { + $resourcesObj = $inputResources | ForEach-Object { + [pscustomobject]@{ + name = $_.Name + version = $_.Version.ToString() + _exists = $false + } } } - - $resources += $resourcesMissing | ForEach-Object { - [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exists = $false + else { + $resources += $resourcesExist | ForEach-Object { + [pscustomobject]@{ + name = $_.Name + version = $_.Version.ToString() + _exists = $true + } } - } - $resourcesObj = if ($scope) { - [pscustomobject]@{ - repositoryName = $repositoryName - scope = $scope - resources = $resources + $resourcesObj = if ($scope) { + [pscustomobject]@{ + repositoryName = $repositoryName + scope = $scope + resources = $resources + } } - } - else { - [pscustomobject]@{ - repositoryName = $repositoryName - resources = $resources + else { + [pscustomobject]@{ + repositoryName = $repositoryName + resources = $resources + } } } From 0a3987ce8606dd52299d95ff95a4ceffdd3e97de Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 28 Jul 2025 13:35:16 -0700 Subject: [PATCH 05/16] Set operations for psresources working --- src/dsc/psresourceget.ps1 | 132 ++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 25 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index b609dec98..9bab1bbcf 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -12,7 +12,6 @@ param( [Parameter(ValueFromPipeline)] $stdinput ) - function Write-Trace { param( [string]$message, @@ -23,7 +22,21 @@ function Write-Trace { $level.ToLower() = $message } | ConvertTo-Json -Compress - $host.ui.WriteErrorLine($trace) + if ($level -eq 'Error') { + $host.ui.WriteErrorLine($trace) + } + elseif ($level -eq 'Warning') { + $host.ui.WriteWarningLine($trace) + } + elseif ($level -eq 'Verbose') { + $host.ui.WriteVerboseLine($trace) + } + elseif ($level -eq 'Debug') { + $host.ui.WriteDebugLine($trace) + } + else { + $host.ui.WriteInformation($trace) + } } # catch any un-caught exception and write it to the error stream @@ -32,10 +45,6 @@ trap { exit 1 } -function GetAllPSResources { - $resources = Get-PSResource -} - function GetOperation { param( [string]$ResourceType @@ -55,13 +64,13 @@ function GetOperation { return $ret } - 'repositories' { return 'Get-PSRepository' } - 'psresource' { return 'Get-PSResource' } + 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } 'psresources' { - - $allPSResources = if ($inputObj.scope) { + $allPSResources = if ($inputObj.scope) { Get-PSResource -Scope $inputObj.Scope - } else { + } + else { Get-PSResource } @@ -97,7 +106,8 @@ function GetOperation { } PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -inputResources $inputObj.resources -repositoryName $inputObj.repositoryName -scope $inputObj.Scope - } + } + default { throw "Unknown ResourceType: $ResourceType" } } } @@ -112,16 +122,89 @@ function ExportOperation { } } - 'repositories' { return 'Get-PSRepository' } - 'psresource' { return 'Get-PSResource' } + 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } 'psresources' { $allPSResources = Get-PSResource PopulatePSResourcesObject -allPSResources $allPSResources - } + } default { throw "Unknown ResourceType: $ResourceType" } } } +function SetPSResources { + param( + $inputObj + ) + + $repositoryName = $inputObj.repositoryName + $scope = $inputObj.scope + + if (-not $scope) { + $scope = 'CurrentUser' + } + + $resourcesToUninstall = @() + $resourcesToInstall = [System.Collections.Generic.Dictionary[string, psobject]]::new() + + Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" + + $inputObj.resources | ForEach-Object { + $resource = $_ + $name = $resource.name + $version = $resource.version + + $getSplat = @{ + Name = $name + Scope = $scope + ErrorAction = 'SilentlyContinue' + } + + $existingResources = if ($repositoryName) { + Get-PSResource @getSplat | Where-Object { $_.Repository -eq $repositoryName } + } + else { + Get-PSResource @getSplat + } + + # uninstall all resources that do not satisfy the version range and install the ones that do + $existingResources | ForEach-Object { + $versionRange = [NuGet.Versioning.VersionRange]::Parse($version) + $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($_.Version.ToString()) + if (-not $versionRange.Satisfies($resourceVersion)) { + if ($resource._exists) { + #$resourcesToInstall += $resource + $key = $resource.Name.ToLowerInvariant() + '-' + $resource.Version.ToLowerInvariant() + if (-not $resourcesToInstall.ContainsKey($key)) { + $resourcesToInstall[$key] = $resource + } + } + + $resourcesToUninstall += $_ + } + else { + if (-not $resource._exists) { + $resourcesToUninstall += $_ + } + } + } + } + + if ($resourcesToUninstall.Count -gt 0) { + Write-Trace -message "Uninstalling resources: $($resourcesToUninstall | ForEach-Object { "$($_.Name) - $($_.Version)" })" -Level Verbose + $resourcesToUninstall | ForEach-Object { + Uninstall-PSResource -Name $_.Name -Scope $scope -ErrorAction Stop + } + } + + if ($resourcesToInstall.Count -gt 0) { + Write-Trace -message "Installing resources: $($resourcesToInstall.Values | ForEach-Object { " $($_.Name) -- $($_.Version) " })" -Level Verbose + $resourcesToInstall.Values | ForEach-Object { + Install-PSResource -Name $_.Name -Version $_.Version -Scope $scope -Repository $repositoryName -ErrorAction Stop + } + } +} + function SetOperation { param( [string]$ResourceType @@ -165,13 +248,12 @@ function SetOperation { return GetOperation -ResourceType $ResourceType } - 'repositories' { return 'Set-PSRepository' } - 'psresource' { return 'Set-PSResource' } - 'psresources' { return 'Set-PSResource' } + 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } + 'psresources' { return SetPSResources -inputObj $inputObj } default { throw "Unknown ResourceType: $ResourceType" } } } - function FilterPSResourcesByRepository { param ( $allPSResources, @@ -247,15 +329,15 @@ function PopulatePSResourcesObject { $resources = $_.Group | ForEach-Object { [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exists = $true + name = $_.Name + version = $_.Version.ToString() + _exists = $true } } $resourcesObj = [pscustomobject]@{ repositoryName = $repoName - resources = $resources + resources = $resources } $resourcesObj | ConvertTo-Json -Compress @@ -297,8 +379,8 @@ function PopulateRepositoryObject { } switch ($Operation.ToLower()) { - 'get' { return (GetOperation -ResourceType $ResourceType) } - 'set' { return (SetOperation -ResourceType $ResourceType) } + 'get' { return (GetOperation -ResourceType $ResourceType) } + 'set' { return (SetOperation -ResourceType $ResourceType) } 'export' { return (ExportOperation -ResourceType $ResourceType) } default { throw "Unknown Operation: $Operation" } } From c4d1a58f4a3ec11dc9ecdb949abd4d91d6051f27 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 28 Jul 2025 13:41:23 -0700 Subject: [PATCH 06/16] fix enums --- src/dsc/psresources.dsc.resource.json | 6 +--- src/dsc/repository.dsc.resource.json | 52 ++++++++++++--------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json index f6c11cc1c..4087d6e6b 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresources.dsc.resource.json @@ -67,13 +67,9 @@ "Scope": { "type": "integer", "description": "", - "x-enumNames": [ + "enum": [ "CurrentUser", "AllUsers" - ], - "enum": [ - 0, - 1 ] }, "PSResource": { diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index 153573a4d..87bafdd11 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -44,50 +44,42 @@ "additionalProperties": false, "properties": { "name": { - "type": [ - "null", - "string" - ] + "type": [ + "null", + "string" + ] }, "uri": { - "type": [ - "null", - "string" - ] + "type": [ + "null", + "string" + ] }, "trusted": { - "type": "boolean" + "type": "boolean" }, "priority": { - "type": "integer" + "type": "integer" }, "repositoryType": { - "$ref": "#/definitions/RepositoryType" + "$ref": "#/definitions/RepositoryType" }, "_exists": { - "type": "boolean" + "type": "boolean" } }, "definitions": { "RepositoryType": { - "type": "integer", - "description": "", - "x-enumNames": [ - "Unknown", - "V2", - "V3", - "Local", - "NugetServer", - "ContainerRegistry" - ], - "enum": [ - 0, - 1, - 2, - 3, - 4, - 5 - ] + "type": "integer", + "description": "", + "enum": [ + "Unknown", + "V2", + "V3", + "Local", + "NugetServer", + "ContainerRegistry" + ] } } } From 060c08313a097d8d9e572d58b2a16a048ebd6d0b Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 28 Jul 2025 13:58:44 -0700 Subject: [PATCH 07/16] Restores setting files --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 501ade606..1dea6f957 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,5 @@ "csharp.semanticHighlighting.enabled": true, "dotnet.automaticallyCreateSolutionInWorkspace": false, "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableRoslynAnalyzers": true, - "sarif-viewer.connectToGithubCodeScanning": "off" + "omnisharp.enableRoslynAnalyzers": true } From 5aab0e8c465233615dc63700850aff33554bae85 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 29 Jul 2025 15:01:27 -0700 Subject: [PATCH 08/16] schema updates via CR --- src/dsc/psresources.dsc.resource.json | 56 ++++++++++++++------------- src/dsc/repository.dsc.resource.json | 32 ++++++++------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json index 4087d6e6b..c1877ea98 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresources.dsc.resource.json @@ -1,6 +1,6 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "description": "This resource manages PowerShell resources using PSResourceGet.", + "description": "Manage PowerShell resources using PSResourceGet.", "type": "Microsoft.PowerShell.PSResourceGet/PSResources", "version": "0.0.1", "get": { @@ -38,35 +38,36 @@ }, "schema": { "embedded": { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-2020-12/schema#", "title": "PSResources", "type": "object", "additionalProperties": false, "properties": { "repositoryName": { - "type": [ - "null", - "string" - ] + "title": "Repository Name", + "description": "The name of the repository from where the resources are acquired.", + "type": "string" }, "scope": { - "$ref": "#/definitions/Scope", + "title": "Scope", + "description": "The scope of the resources. Can be 'CurrentUser' or 'AllUsers'.", + "$ref": "#/$defs/Scope", "writeOnly": true }, "resources": { - "type": [ - "array", - "null" - ], + "title": "Resources", + "description": "The list of resources to manage.", + "type": "array", "items": { - "$ref": "#/definitions/PSResource" + "$ref": "#/$defs/PSResource" } } }, - "definitions": { + "$defs": { "Scope": { "type": "integer", - "description": "", + "title": "Scope", + "description": "Scope of the resource installation.", "enum": [ "CurrentUser", "AllUsers" @@ -77,28 +78,29 @@ "additionalProperties": false, "properties": { "name": { - "type": [ - "null", - "string" - ] + "title": "Name", + "description": "The name of the resource.", + "type": "string" }, "version": { - "type": [ - "null", - "string" - ] + "title": "Version", + "description": "The version range of the resource.", + "type": "string" }, "scope": { - "$ref": "#/definitions/Scope", + "title": "Scope", + "description": "The scope of the resource. Can be 'CurrentUser' or 'AllUsers'.", + "$ref": "#/$defs/Scope", "writeOnly": true }, "repositoryName": { - "type": [ - "null", - "string" - ] + "title": "Repository Name", + "description": "The name of the repository from where the resource is acquired.", + "type": "string" }, "preRelease": { + "title": "Pre-Release version", + "description": "Indicates whether to include pre-release versions of the resource.", "type": "boolean", "writeOnly": true }, diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index 87bafdd11..67cfbcda5 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -1,6 +1,6 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "description": "This resource manages PowerShell repositories using PSResourceGet.", + "description": "Manage PowerShell repositories using PSResourceGet.", "type": "Microsoft.PowerShell.PSResourceGet/Repository", "version": "0.0.1", "get": { @@ -38,40 +38,46 @@ }, "schema": { "embedded": { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-2020-12/schema#", "title": "Repository", + "description": "A PowerShell Resource repository from where to acquire the resources.", "type": "object", "additionalProperties": false, "properties": { "name": { - "type": [ - "null", - "string" - ] + "title": "Name", + "description": "The name of the repository.", + "type": "string" }, "uri": { - "type": [ - "null", - "string" - ] + "title": "URI", + "description": "The URI of the repository.", + "type": "string" }, "trusted": { + "title": "Trusted", + "description": "Indicates whether the repository is trusted.", "type": "boolean" }, "priority": { + "title": "Priority", + "description": "The priority of the repository. Lower numbers indicate higher priority.", "type": "integer" }, "repositoryType": { - "$ref": "#/definitions/RepositoryType" + "title": "Repository Type", + "description": "The type of the repository.", + "$ref": "#/$defs/RepositoryType" }, "_exists": { "type": "boolean" } }, - "definitions": { + "$defs": { "RepositoryType": { "type": "integer", - "description": "", + "title": "Repository Type", + "description": "The type of the repository. Can be 'Unknown', 'V2', 'V3', 'Local', 'NugetServer', or 'ContainerRegistry'.", "enum": [ "Unknown", "V2", From 48081f86d6058fb6c57a5d3e7ca1425e11e3ca9b Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 30 Jul 2025 14:22:37 -0700 Subject: [PATCH 09/16] Schema updates --- src/dsc/psresourceget.ps1 | 16 ++++---- src/dsc/psresources.dsc.resource.json | 54 ++++++++++++++++++++------- src/dsc/repository.dsc.resource.json | 35 ++++++++++++++--- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 9bab1bbcf..4469b263d 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -172,7 +172,7 @@ function SetPSResources { $versionRange = [NuGet.Versioning.VersionRange]::Parse($version) $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($_.Version.ToString()) if (-not $versionRange.Satisfies($resourceVersion)) { - if ($resource._exists) { + if ($resource._exist) { #$resourcesToInstall += $resource $key = $resource.Name.ToLowerInvariant() + '-' + $resource.Version.ToLowerInvariant() if (-not $resourcesToInstall.ContainsKey($key)) { @@ -183,7 +183,7 @@ function SetPSResources { $resourcesToUninstall += $_ } else { - if (-not $resource._exists) { + if (-not $resource._exist) { $resourcesToUninstall += $_ } } @@ -285,7 +285,7 @@ function PopulatePSResourcesObjectByRepository { [pscustomobject]@{ name = $_.Name version = $_.Version.ToString() - _exists = $false + _exist = $false } } } @@ -294,7 +294,7 @@ function PopulatePSResourcesObjectByRepository { [pscustomobject]@{ name = $_.Name version = $_.Version.ToString() - _exists = $true + _exist = $true } } @@ -331,7 +331,7 @@ function PopulatePSResourcesObject { [pscustomobject]@{ name = $_.Name version = $_.Version.ToString() - _exists = $true + _exist = $true } } @@ -350,7 +350,7 @@ function PopulateRepositoryObject { ) $repository = if (-not $RepositoryInfo) { - Write-Trace -message "RepositoryInfo is null or empty. Returning _exists = false" -Level Information + Write-Trace -message "RepositoryInfo is null or empty. Returning _exist = false" -Level Information $inputJson = $stdinput | ConvertFrom-Json -ErrorAction Stop @@ -360,7 +360,7 @@ function PopulateRepositoryObject { trusted = $inputJson.Trusted priority = $inputJson.Priority repositoryType = $inputJson.repositoryType - _exists = $false + _exist = $false } } else { @@ -371,7 +371,7 @@ function PopulateRepositoryObject { trusted = $RepositoryInfo.Trusted priority = $RepositoryInfo.Priority repositoryType = $RepositoryInfo.ApiVersion - _exists = $true + _exist = $true } } diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json index c1877ea98..bda8e923e 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresources.dsc.resource.json @@ -1,7 +1,7 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "description": "Manage PowerShell resources using PSResourceGet.", - "type": "Microsoft.PowerShell.PSResourceGet/PSResources", + "type": "Microsoft.PowerShell.PSResourceGet/PSResourceList", "version": "0.0.1", "get": { "executable": "pwsh", @@ -10,7 +10,7 @@ "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./psresourceget.ps1 -resourcetype 'psresources' -operation 'get'" + "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation 'get'" ], "input": "stdin" }, @@ -21,9 +21,10 @@ "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./psresourceget.ps1 -resourcetype 'psresources' -operation set" + "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set" ], - "input": "stdin" + "input": "stdin", + "return": "stateAndDiff" }, "export": { "executable": "pwsh", @@ -32,14 +33,26 @@ "-NonInteractive", "-NoProfile", "-Command", - "./psresourceget.ps1 -resourcetype 'psresources' -operation export" + "./psresourceget.ps1 -resourcetype 'psresourcelist' -operation export" ], "input": "stdin" }, + "test": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation test" + ], + "input": "stdin", + "return": "stateAndDiff" + }, "schema": { "embedded": { "$schema": "http://json-schema.org/draft-2020-12/schema#", - "title": "PSResources", + "title": "PSResourceList", "type": "object", "additionalProperties": false, "properties": { @@ -50,8 +63,9 @@ }, "scope": { "title": "Scope", - "description": "The scope of the resources. Can be 'CurrentUser' or 'AllUsers'.", + "description": "The scope of the resources. Can be 'CurrentUser' or 'AllUsers'. Defaults to 'CurrentUser'.", "$ref": "#/$defs/Scope", + "default": "CurrentUser", "writeOnly": true }, "resources": { @@ -65,7 +79,7 @@ }, "$defs": { "Scope": { - "type": "integer", + "type": "string", "title": "Scope", "description": "Scope of the resource installation.", "enum": [ @@ -76,6 +90,9 @@ "PSResource": { "type": "object", "additionalProperties": false, + "required": [ + "name" + ], "properties": { "name": { "title": "Name", @@ -86,12 +103,13 @@ "title": "Version", "description": "The version range of the resource.", "type": "string" + // Look at nuget versioning for format + // returning back semantic version by adding prerelease }, "scope": { "title": "Scope", "description": "The scope of the resource. Can be 'CurrentUser' or 'AllUsers'.", - "$ref": "#/$defs/Scope", - "writeOnly": true + "$ref": "#/$defs/Scope" }, "repositoryName": { "title": "Repository Name", @@ -102,11 +120,21 @@ "title": "Pre-Release version", "description": "Indicates whether to include pre-release versions of the resource.", "type": "boolean", - "writeOnly": true + "default": false + }, + "_exist": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" }, - "_exists": { - "type": "boolean" + "_inDesiredState": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" } + + // for test and set everything + // 2 json lines + // state == current state of object + // diff == array of properties that are different + + // for other operations, DONOT return _inDesiredState } } } diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index 67cfbcda5..b6e7fdbea 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -43,6 +43,28 @@ "description": "A PowerShell Resource repository from where to acquire the resources.", "type": "object", "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "_exist": { + "const": false + } + } + }, + "then": { + "required": [ + "name" + ] + }, + "else": { + "required": [ + "name", + "uri" + ] + } + } + ], "properties": { "name": { "title": "Name", @@ -52,7 +74,8 @@ "uri": { "title": "URI", "description": "The URI of the repository.", - "type": "string" + "type": "string", + "format": "uri" }, "trusted": { "title": "Trusted", @@ -62,20 +85,22 @@ "priority": { "title": "Priority", "description": "The priority of the repository. Lower numbers indicate higher priority.", - "type": "integer" + "type": "integer", + "minimum": 0, + "maximum": 100 }, "repositoryType": { "title": "Repository Type", "description": "The type of the repository.", "$ref": "#/$defs/RepositoryType" }, - "_exists": { - "type": "boolean" + "_exist": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" } }, "$defs": { "RepositoryType": { - "type": "integer", + "type": "string", "title": "Repository Type", "description": "The type of the repository. Can be 'Unknown', 'V2', 'V3', 'Local', 'NugetServer', or 'ContainerRegistry'.", "enum": [ From 7701b434273a54cbafd4a3480dce1313d246a08f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 30 Jul 2025 14:32:19 -0700 Subject: [PATCH 10/16] Fix json and add regex --- src/dsc/psresourceget.ps1 | 12 ++++++++++++ src/dsc/psresources.dsc.resource.json | 12 ++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 4469b263d..ca31fef15 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -50,6 +50,8 @@ function GetOperation { [string]$ResourceType ) + ## TODO : ensure that version returned includes pre-release versions + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop switch ($ResourceType) { @@ -210,6 +212,16 @@ function SetOperation { [string]$ResourceType ) + <# TODO + // for test and set everything + // 2 json lines + // state == current state of object + // diff == array of properties that are different + + // for other operations, DONOT return _inDesiredState + + #> + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop switch ($ResourceType) { diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json index bda8e923e..db8e090e4 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresources.dsc.resource.json @@ -102,9 +102,8 @@ "version": { "title": "Version", "description": "The version range of the resource.", - "type": "string" - // Look at nuget versioning for format - // returning back semantic version by adding prerelease + "type": "string", + "pattern": "^(\\[|\\()\\s*\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?\\s*(,\\s*(\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)?\\s*(\\]|\\)))?$" }, "scope": { "title": "Scope", @@ -128,13 +127,6 @@ "_inDesiredState": { "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" } - - // for test and set everything - // 2 json lines - // state == current state of object - // diff == array of properties that are different - - // for other operations, DONOT return _inDesiredState } } } From 7d0eaacf402906f29967d5b620c72eacfe626993 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 30 Jul 2025 14:38:35 -0700 Subject: [PATCH 11/16] Add tags --- src/dsc/psresourceget.ps1 | 1 + src/dsc/psresources.dsc.resource.json | 5 +++++ src/dsc/repository.dsc.resource.json | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index ca31fef15..2a6d13bc7 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -12,6 +12,7 @@ param( [Parameter(ValueFromPipeline)] $stdinput ) + function Write-Trace { param( [string]$message, diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresources.dsc.resource.json index db8e090e4..3e1d75f6e 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresources.dsc.resource.json @@ -1,6 +1,11 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "description": "Manage PowerShell resources using PSResourceGet.", + "tags": [ + "linux", + "windows", + "macos" + ], "type": "Microsoft.PowerShell.PSResourceGet/PSResourceList", "version": "0.0.1", "get": { diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index b6e7fdbea..5213d7f8a 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -1,6 +1,11 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "description": "Manage PowerShell repositories using PSResourceGet.", + "tags": [ + "linux", + "windows", + "macos" + ], "type": "Microsoft.PowerShell.PSResourceGet/Repository", "version": "0.0.1", "get": { From 55ad3ae1fb760d0190c06c796b7902ff268b92b1 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 30 Jul 2025 15:14:01 -0700 Subject: [PATCH 12/16] Address CR feedback --- src/dsc/psresourceget.ps1 | 35 ++++++------------- ....json => psresourcelist.dsc.resource.json} | 8 ++++- src/dsc/repository.dsc.resource.json | 7 +++- 3 files changed, 23 insertions(+), 27 deletions(-) rename src/dsc/{psresources.dsc.resource.json => psresourcelist.dsc.resource.json} (95%) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 2a6d13bc7..4511eb710 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -7,7 +7,7 @@ param( [ValidateSet('repository', 'psresource', 'repositories', 'psresources')] [string]$ResourceType, [Parameter(Mandatory = $true)] - [ValidateSet('get', 'set', 'export', 'test')] + [ValidateSet('get', 'set', 'test', 'export')] [string]$Operation, [Parameter(ValueFromPipeline)] $stdinput @@ -16,28 +16,17 @@ param( function Write-Trace { param( [string]$message, - [string]$level = 'Error' + + [ValidateSet('error', 'warn', 'info', 'debug', 'trace')] + [string]$level = 'trace' ) $trace = [pscustomobject]@{ $level.ToLower() = $message } | ConvertTo-Json -Compress - if ($level -eq 'Error') { - $host.ui.WriteErrorLine($trace) - } - elseif ($level -eq 'Warning') { - $host.ui.WriteWarningLine($trace) - } - elseif ($level -eq 'Verbose') { - $host.ui.WriteVerboseLine($trace) - } - elseif ($level -eq 'Debug') { - $host.ui.WriteDebugLine($trace) - } - else { - $host.ui.WriteInformation($trace) - } + $host.ui.WriteInformation($trace) + } # catch any un-caught exception and write it to the error stream @@ -194,14 +183,14 @@ function SetPSResources { } if ($resourcesToUninstall.Count -gt 0) { - Write-Trace -message "Uninstalling resources: $($resourcesToUninstall | ForEach-Object { "$($_.Name) - $($_.Version)" })" -Level Verbose + Write-Trace -message "Uninstalling resources: $($resourcesToUninstall | ForEach-Object { "$($_.Name) - $($_.Version)" })" $resourcesToUninstall | ForEach-Object { Uninstall-PSResource -Name $_.Name -Scope $scope -ErrorAction Stop } } if ($resourcesToInstall.Count -gt 0) { - Write-Trace -message "Installing resources: $($resourcesToInstall.Values | ForEach-Object { " $($_.Name) -- $($_.Version) " })" -Level Verbose + Write-Trace -message "Installing resources: $($resourcesToInstall.Values | ForEach-Object { " $($_.Name) -- $($_.Version) " })" $resourcesToInstall.Values | ForEach-Object { Install-PSResource -Name $_.Name -Version $_.Version -Scope $scope -Repository $repositoryName -ErrorAction Stop } @@ -363,21 +352,17 @@ function PopulateRepositoryObject { ) $repository = if (-not $RepositoryInfo) { - Write-Trace -message "RepositoryInfo is null or empty. Returning _exist = false" -Level Information + Write-Trace -message "RepositoryInfo is null or empty. Returning _exist = false" $inputJson = $stdinput | ConvertFrom-Json -ErrorAction Stop [pscustomobject]@{ name = $inputJson.Name - uri = $inputJson.Uri - trusted = $inputJson.Trusted - priority = $inputJson.Priority - repositoryType = $inputJson.repositoryType _exist = $false } } else { - Write-Trace -message "Populating repository object for: $($RepositoryInfo.Name)" -Level Verbose + Write-Trace -message "Populating repository object for: $($RepositoryInfo.Name)" [pscustomobject]@{ name = $RepositoryInfo.Name uri = $RepositoryInfo.Uri diff --git a/src/dsc/psresources.dsc.resource.json b/src/dsc/psresourcelist.dsc.resource.json similarity index 95% rename from src/dsc/psresources.dsc.resource.json rename to src/dsc/psresourcelist.dsc.resource.json index 3e1d75f6e..f3d99955d 100644 --- a/src/dsc/psresources.dsc.resource.json +++ b/src/dsc/psresourcelist.dsc.resource.json @@ -4,7 +4,9 @@ "tags": [ "linux", "windows", - "macos" + "macos", + "powershell", + "nuget" ], "type": "Microsoft.PowerShell.PSResourceGet/PSResourceList", "version": "0.0.1", @@ -14,6 +16,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation 'get'" ], @@ -25,6 +28,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set" ], @@ -37,6 +41,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "./psresourceget.ps1 -resourcetype 'psresourcelist' -operation export" ], @@ -48,6 +53,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation test" ], diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index 5213d7f8a..9cbfabc01 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -4,7 +4,9 @@ "tags": [ "linux", "windows", - "macos" + "macos", + "powershell", + "nuget" ], "type": "Microsoft.PowerShell.PSResourceGet/Repository", "version": "0.0.1", @@ -14,6 +16,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation 'get'" ], @@ -25,6 +28,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation set" ], @@ -36,6 +40,7 @@ "-NoLogo", "-NonInteractive", "-NoProfile", + "-ExecutionPolicy Bypass", "-Command", "./psresourceget.ps1 -resourcetype 'repository' -operation export" ], From f319c309f509360ecbf6fb3b9e2f8d37c395d60f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 30 Jul 2025 16:35:00 -0700 Subject: [PATCH 13/16] update resource names --- src/dsc/psresourceget.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index 4511eb710..ac35776be 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -4,7 +4,7 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('repository', 'psresource', 'repositories', 'psresources')] + [ValidateSet('repository', 'psresource', 'repositorylist', 'psresourcelist')] [string]$ResourceType, [Parameter(Mandatory = $true)] [ValidateSet('get', 'set', 'test', 'export')] @@ -56,9 +56,9 @@ function GetOperation { return $ret } - 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } - 'psresources' { + 'psresourcelist' { $allPSResources = if ($inputObj.scope) { Get-PSResource -Scope $inputObj.Scope } @@ -114,9 +114,9 @@ function ExportOperation { } } - 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } - 'psresources' { + 'psresourcelist' { $allPSResources = Get-PSResource PopulatePSResourcesObject -allPSResources $allPSResources } @@ -250,9 +250,9 @@ function SetOperation { return GetOperation -ResourceType $ResourceType } - 'repositories' { throw [System.NotImplementedException]::new("Get operation is not implemented for Repositories resource.") } + 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } - 'psresources' { return SetPSResources -inputObj $inputObj } + 'psresourcelist' { return SetPSResources -inputObj $inputObj } default { throw "Unknown ResourceType: $ResourceType" } } } From dfa7fbfb93fde1812aa65448078ea48a7487479e Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 5 Aug 2025 15:28:16 -0700 Subject: [PATCH 14/16] Refactor --- src/dsc/psresourceget.ps1 | 187 ++++++++++++++++++----- src/dsc/psresourcelist.dsc.resource.json | 2 +- src/dsc/repository.dsc.resource.json | 36 ++++- 3 files changed, 182 insertions(+), 43 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index ac35776be..cfd151511 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -7,12 +7,85 @@ param( [ValidateSet('repository', 'psresource', 'repositorylist', 'psresourcelist')] [string]$ResourceType, [Parameter(Mandatory = $true)] - [ValidateSet('get', 'set', 'test', 'export')] + [ValidateSet('get', 'set', 'test', 'delete', 'export')] [string]$Operation, [Parameter(ValueFromPipeline)] $stdinput ) +enum Scope { + CurrentUser + AllUsers +} + +class PSResource { + [string]$name + [string]$version + [Scope]$scope + [string]$repositoryName + [bool]$preRelease + [bool]$_exist + [bool]$_inDesiredState + + PSResource([string]$name, [string]$version, [Scope]$scope, [string]$repositoryName, [bool]$preRelease) { + $this.name = $name + $this.version = $version + $this.scope = $scope + $this.repositoryName = $repositoryName + $this.preRelease = $preRelease + $this._exist = $true + } +} + +class PSResourceList { + [string]$repositoryName + [Scope]$scope + [PSResource[]]$resources + + PSResourceList([string]$repositoryName, [Scope]$scope, [PSResource[]]$resources) { + $this.repositoryName = $repositoryName + $this.scope = $scope + $this.resources = $resources + } +} + +class Repository { + [string]$name + [string]$uri + [bool]$trusted + [int]$priority + [string]$repositoryType + [bool]$_exist + + Repository([string]$name) { + $this.name = $name + $this._exist = $false + } + + Repository([string]$name, [string]$uri, [bool]$trusted, [int]$priority, [string]$repositoryType) { + $this.name = $name + $this.uri = $uri + $this.trusted = $trusted + $this.priority = $priority + $this.repositoryType = $repositoryType + $this._exist = $true + } + + Repository([PSCustomObject]$repositoryInfo) { + $this.name = $repositoryInfo.Name + $this.uri = $repositoryInfo.Uri + $this.trusted = $repositoryInfo.Trusted + $this.priority = $repositoryInfo.Priority + $this.repositoryType = $repositoryInfo.ApiVersion + $this._exist = $true + } + + Repository([string]$name, [bool]$exist) { + $this.name = $name + $this._exist = $exist + } +} + function Write-Trace { param( [string]$message, @@ -25,8 +98,12 @@ function Write-Trace { $level.ToLower() = $message } | ConvertTo-Json -Compress - $host.ui.WriteInformation($trace) - + if($env:SKIP_TRACE) { + $host.ui.WriteVerboseLine($trace) + } + else { + $host.ui.WriteErrorLine($trace) + } } # catch any un-caught exception and write it to the error stream @@ -46,14 +123,30 @@ function GetOperation { switch ($ResourceType) { 'repository' { - $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorVariable err -ErrorAction SilentlyContinue + $inputRepository = [Repository]::new($inputObj) - if ($err.FullyQualifiedErrorId -eq 'ErrorGettingSpecifiedRepo,Microsoft.PowerShell.PSResourceGet.Cmdlets.GetPSResourceRepository') { - return PopulateRepositoryObject -RepositoryInfo $null + $rep = Get-PSResourceRepository -Name $inputRepository.Name -ErrorVariable err -ErrorAction SilentlyContinue + + $ret = if ($err.FullyQualifiedErrorId -eq 'ErrorGettingSpecifiedRepo,Microsoft.PowerShell.PSResourceGet.Cmdlets.GetPSResourceRepository') { + Write-Trace -message "Repository not found: $($inputRepository.Name). Returning _exist = false" + [Repository]::new( + $InputRepository.Name, + $false + ) + } + else { + [Repository]::new( + $rep.Name, + $rep.Uri, + $rep.Trusted, + $rep.Priority, + $rep.ApiVersion + ) + + Write-Trace -message "Returning repository object for: $($ret.Name)" } - $ret = PopulateRepositoryObject -RepositoryInfo $rep - return $ret + return ( $ret | ConvertTo-Json -Compress ) } 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } @@ -107,11 +200,23 @@ function GetOperation { function ExportOperation { switch ($ResourceType) { 'repository' { - $rep = Get-PSResourceRepository -ErrorAction Stop + $rep = Get-PSResourceRepository -ErrorAction SilentlyContinue + + if (-not $rep) { + Write-Trace -message "No repositories found. Returning empty array." -level info + return @() + } $rep | ForEach-Object { - PopulateRepositoryObject -RepositoryInfo $_ + [Repository]::new( + $_.Name, + $_.Uri, + $_.Trusted, + $_.Priority, + $_.ApiVersion + ) | ConvertTo-Json -Compress } + } 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } @@ -256,6 +361,39 @@ function SetOperation { default { throw "Unknown ResourceType: $ResourceType" } } } + +function DeleteOperation { + param( + [string]$ResourceType + ) + + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop + switch ($ResourceType) { + 'repository' { + if (-not $inputObj._exist -ne $false) { + throw "_exist property is not set to false for the repository. Cannot delete." + } + + $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorAction SilentlyContinue + + if ($null -ne $rep) { + Unregister-PSResourceRepository -Name $inputObj.Name + } + else { + Write-Trace -message "Repository not found: $($inputObj.Name). Nothing to delete." -level info + } + + return GetOperation -ResourceType $ResourceType + } + + 'repositorylist' { throw [System.NotImplementedException]::new("Delete operation is not implemented for RepositoryList resource.") } + 'psresource' { throw [System.NotImplementedException]::new("Delete operation is not implemented for PSResource resource.") } + 'psresourcelist' { throw [System.NotImplementedException]::new("Delete operation is not implemented for PSResourceList resource.") } + default { throw "Unknown ResourceType: $ResourceType" } + } +} + + function FilterPSResourcesByRepository { param ( $allPSResources, @@ -346,39 +484,12 @@ function PopulatePSResourcesObject { } } -function PopulateRepositoryObject { - param( - $RepositoryInfo - ) - - $repository = if (-not $RepositoryInfo) { - Write-Trace -message "RepositoryInfo is null or empty. Returning _exist = false" - - $inputJson = $stdinput | ConvertFrom-Json -ErrorAction Stop - [pscustomobject]@{ - name = $inputJson.Name - _exist = $false - } - } - else { - Write-Trace -message "Populating repository object for: $($RepositoryInfo.Name)" - [pscustomobject]@{ - name = $RepositoryInfo.Name - uri = $RepositoryInfo.Uri - trusted = $RepositoryInfo.Trusted - priority = $RepositoryInfo.Priority - repositoryType = $RepositoryInfo.ApiVersion - _exist = $true - } - } - - return ($repository | ConvertTo-Json -Compress) -} switch ($Operation.ToLower()) { 'get' { return (GetOperation -ResourceType $ResourceType) } 'set' { return (SetOperation -ResourceType $ResourceType) } 'export' { return (ExportOperation -ResourceType $ResourceType) } + 'delete' { return (DeleteOperation -ResourceType $ResourceType) } default { throw "Unknown Operation: $Operation" } } diff --git a/src/dsc/psresourcelist.dsc.resource.json b/src/dsc/psresourcelist.dsc.resource.json index f3d99955d..68bd47f49 100644 --- a/src/dsc/psresourcelist.dsc.resource.json +++ b/src/dsc/psresourcelist.dsc.resource.json @@ -62,7 +62,7 @@ }, "schema": { "embedded": { - "$schema": "http://json-schema.org/draft-2020-12/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "PSResourceList", "type": "object", "additionalProperties": false, diff --git a/src/dsc/repository.dsc.resource.json b/src/dsc/repository.dsc.resource.json index 9cbfabc01..a54978d15 100644 --- a/src/dsc/repository.dsc.resource.json +++ b/src/dsc/repository.dsc.resource.json @@ -16,7 +16,8 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation 'get'" ], @@ -28,19 +29,34 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation set" ], "input": "stdin" }, + "delete": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation delete" + ], + "input": "stdin" + }, "export": { "executable": "pwsh", "args": [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "./psresourceget.ps1 -resourcetype 'repository' -operation export" ], @@ -48,7 +64,7 @@ }, "schema": { "embedded": { - "$schema": "http://json-schema.org/draft-2020-12/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Repository", "description": "A PowerShell Resource repository from where to acquire the resources.", "type": "object", @@ -121,6 +137,18 @@ "NugetServer", "ContainerRegistry" ] + }, + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json", + "title": "Instance should exist", + "description": "Indicates whether the DSC resource instance should exist.", + "type": "boolean", + "default": true, + "enum": [ + false, + true + ] } } } From 0880be4e899c3c0846de252fef13fd169fdd5cfd Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 5 Aug 2025 15:45:53 -0700 Subject: [PATCH 15/16] Refactor repository --- src/dsc/psresourceget.ps1 | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index cfd151511..c508ea6be 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -323,33 +323,27 @@ function SetOperation { 'repository' { $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorAction SilentlyContinue - $splatt = @{} - - if ($inputObj.Name) { - $splatt['Name'] = $inputObj.Name - } - - if ($inputObj.Uri) { - $splatt['Uri'] = $inputObj.Uri - } - - if ($inputObj.Trusted) { - $splatt['Trusted'] = $inputObj.Trusted - } + $properties = @('Name', 'Uri', 'Trusted', 'Priority', 'RepositoryType') - if ($null -ne $inputObj.Priority ) { - $splatt['Priority'] = $inputObj.Priority - } + $splatt = @{} - if ($inputObj.repositoryType) { - $splatt['ApiVersion'] = $inputObj.repositoryType + foreach($property in $properties) { + if ($null -ne $inputObj.PSObject.Properties[$property]) { + $splatt[$property] = $inputObj.$property + } } if ($null -eq $rep) { Register-PSResourceRepository @splatt } else { - Set-PSResourceRepository @splatt + if ($inputObj._exist -eq $false) { + Write-Trace -message "Repository $($inputObj.Name) exists and _exist is false. Deleting it." -level info + Unregister-PSResourceRepository -Name $inputObj.Name + } + else { + Set-PSResourceRepository @splatt + } } return GetOperation -ResourceType $ResourceType From feafc53987d8943d1eb0fedb8c32d8e6dd5a3177 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 11 Aug 2025 09:57:20 -0700 Subject: [PATCH 16/16] Major refactor --- src/dsc/psresourceget.ps1 | 441 +++++++++++++++++------ src/dsc/psresourcelist.dsc.resource.json | 57 ++- 2 files changed, 365 insertions(+), 133 deletions(-) diff --git a/src/dsc/psresourceget.ps1 b/src/dsc/psresourceget.ps1 index c508ea6be..b76df1295 100644 --- a/src/dsc/psresourceget.ps1 +++ b/src/dsc/psresourceget.ps1 @@ -35,18 +35,104 @@ class PSResource { $this.preRelease = $preRelease $this._exist = $true } + + PSResource([string]$name) { + $this.name = $name + $this._exist = $false + $this._inDesiredState = $true + } + + [bool] IsInDesiredState([PSResource] $other) { + $retValue = $true + + if ($this.name -ne $other.name) { + Write-Trace -message "Name mismatch: $($this.name) vs $($other.name)" -level warn + $retValue = $false + } + elseif ($null -ne $this.version -and $null -ne $other.version -and -not (SatisfiesVersion -version $this.version -versionRange $other.version)) { + Write-Trace -message "Version mismatch: $($this.version) vs $($other.version)" -level warn + $retValue = $false + } + elseif ($null -ne $this.scope -and $this.scope -ne $other.scope) { + Write-Trace -message "Scope mismatch: $($this.scope) vs $($other.scope)" -level warn + $retValue = $false + } + elseif ($null -ne $this.repositoryName -and $this.repositoryName -ne $other.repositoryName) { + Write-Trace -message "Repository mismatch: $($this.repositoryName) vs $($other.repositoryName)" -level warn + $retValue = $false + } + elseif ($null -ne $this.preRelease -and $this.preRelease -ne $other.preRelease) { + Write-Trace -message "PreRelease mismatch: $($this.preRelease) vs $($other.preRelease)" -level warn + $retValue = $false + } + elseif ($this._exist -ne $other._exist) { + Write-Trace -message "_exist mismatch: $($this._exist) vs $($other._exist)" -level warn + $retValue = $false + } + + return $retValue + } + + [string] ToJson() { + return ($this | Select-Object -ExcludeProperty _inDesiredState | ConvertTo-Json -Compress) + } + + [string] ToJsonForTest() { + return ($this | Select-Object -ExcludeProperty version, preRelease, repositoryName, scope | ConvertTo-Json -Compress) + } } class PSResourceList { [string]$repositoryName - [Scope]$scope [PSResource[]]$resources + [bool]$_exist + [bool]$_inDesiredState - PSResourceList([string]$repositoryName, [Scope]$scope, [PSResource[]]$resources) { + PSResourceList([string]$repositoryName, [PSResource[]]$resources) { $this.repositoryName = $repositoryName - $this.scope = $scope $this.resources = $resources } + + [bool] IsInDesiredState([PSResourceList] $other) { + if ($this.repositoryName -ne $other.repositoryName) { + Write-Trace -message "RepositoryName mismatch: $($this.repositoryName) vs $($other.repositoryName)" -level warn + return $false + } + + if ($null -ne $this.resources -and $this.resources.Count -ne $other.resources.Count) { + Write-Trace -message "Resources count mismatch: $($this.resources.Count) vs $($other.resources.Count)" -level warn + return $false + } + + foreach ($otherResource in $other.resources) { + $found = $false + foreach ($resource in $this.resources) { + if ($resource.IsInDesiredState($otherResource)) { + $found = $true + break + } + } + + if (-not $found) { + Write-Trace -message "Resource mismatch for: $($otherResource.name)" -level warn + return $false + } + } + + return $true + } + + [string] ToJson() { + $resourceJson = ($this.resources | ForEach-Object { $_.ToJson() }) -join ',' + $resourceJson = "[$resourceJson]" + $jsonString = "{'repositoryName': '$($this.repositoryName)','resources': $resourceJson}" + $jsonString = $jsonString -replace "'", '"' + return $jsonString | ConvertFrom-Json | ConvertTo-Json -Compress + } + + [string] ToJsonForTest() { + return ($this | Select-Object -ExcludeProperty resources | ConvertTo-Json -Compress) + } } class Repository { @@ -84,6 +170,10 @@ class Repository { $this.name = $name $this._exist = $exist } + + [string] ToJson() { + return ($this | ConvertTo-Json -Compress) + } } function Write-Trace { @@ -106,19 +196,133 @@ function Write-Trace { } } +function SatisfiesVersion { + param( + [string]$version, + [string]$versionRange + ) + + Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" + + try { + $versionRangeObj = [NuGet.Versioning.VersionRange]::Parse($versionRange) + $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($version) + return $versionRangeObj.Satisfies($resourceVersion) + } + catch { + Write-Trace -message "Error parsing version or version range: $($_.Exception.Message)" -level error + return $false + } +} + +function ConvertInputToPSResource( + [PSCustomObject]$inputObj, + [string]$repositoryName = $null +) { + $scope = if ($inputObj.Scope) { [Scope]$inputObj.Scope } else { [Scope]::CurrentUser } + + return [PSResource]::new( + $inputObj.Name, + $inputObj.Version, + $scope, + $inputObj.repositoryName ? $inputObj.repositoryName : $repositoryName, + $inputObj.PreRelease + ) +} + # catch any un-caught exception and write it to the error stream trap { Write-Trace -Level Error -message $_.Exception.Message exit 1 } +function GetPSResourceList { + param( + [PSCustomObject]$inputObj + ) + + $inputResources = @() + $inputResources += if ($inputObj.resources) { + $inputObj.resources | ForEach-Object { + ConvertInputToPSResource -inputObj $_ -repositoryName $inputObj.repositoryName + } + } + + $inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources) + + $allPSResources = @() + + if ($inputPSResourceList.repositoryName) { + $currentUserPSResources = Get-PSResource -Scope CurrentUser -ErrorAction SilentlyContinue | Where-Object { $_.Repository -eq $inputPSResourceList.RepositoryName } + $allUsersPSResources = Get-PSResource -Scope AllUsers -ErrorAction SilentlyContinue | Where-Object { $_.Repository -eq $inputPSResourceList.RepositoryName } + } + + $allPSResources += $currentUserPSResources | ForEach-Object { + [PSResource]::new( + $_.Name, + $_.Prerelease ? $_.Version.ToString() + "-" + $_.Prerelease : $_.Version.ToString(), + [Scope]::CurrentUser, + $_.Repository, + $_.PreRelease + ) + } + + $allPSResources += $allUsersPSResources | ForEach-Object { + [PSResource]::new( + $_.Name, + $_.Prerelease ? $_.Version.ToString() + "-" + $_.Prerelease : $_.Version.ToString(), + [Scope]"AllUsers", + $_.Repository, + $_.PreRelease ? $true : $false + ) + } + + $resourcesExist = @() + + ##Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" + + foreach ($resource in $allPSResources) { + foreach ($inputResource in $inputResources) { + if ($resource.Name -eq $inputResource.Name) { + if ($inputResource.Version) { + # Use the NuGet.Versioning package if available, otherwise do a simple comparison + try { + # $versionRange = [NuGet.Versioning.VersionRange]::Parse($inputResource.Version) + + # $resourceVersion = if ($resource.PreRelease) { + # [NuGet.Versioning.NuGetVersion]::Parse($resource.Version.ToString() + "-" + $resource.PreRelease) + # } + # else { + # [NuGet.Versioning.NuGetVersion]::Parse($resource.Version.ToString()) + # } + + # if ($versionRange.Satisfies($resourceVersion)) { + # $resourcesExist += $resource + # } + + if (SatisfiesVersion -version $resource.Version -versionRange $inputResource.Version) { + $resourcesExist += $resource + } + } + catch { + # Fallback: simple string comparison (not full NuGet range support) + if ($resource.Version.ToString() -eq $inputResource.Version) { + $resourcesExist += $resource + } + } + } + } + } + } + + PopulatePSResourceListObjectByRepository -resourcesExist $resourcesExist -inputResources $inputResources -repositoryName $inputPSResourceList.RepositoryName +} + function GetOperation { param( [string]$ResourceType ) - ## TODO : ensure that version returned includes pre-release versions - $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop switch ($ResourceType) { @@ -146,51 +350,67 @@ function GetOperation { Write-Trace -message "Returning repository object for: $($ret.Name)" } - return ( $ret | ConvertTo-Json -Compress ) + return ( $ret.ToJson() ) } 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } 'psresourcelist' { - $allPSResources = if ($inputObj.scope) { - Get-PSResource -Scope $inputObj.Scope - } - else { - Get-PSResource - } + (GetPSResourceList -inputObj $inputObj).ToJson() + } - if ($inputObj.repositoryName) { - $allPSResources = FilterPSResourcesByRepository -allPSResources $allPSResources -repositoryName $inputObj.repositoryName - } + default { throw "Unknown ResourceType: $ResourceType" } + } +} - $resourcesExist = @() - - Add-Type -AssemblyName "$PSScriptRoot/dependencies/NuGet.Versioning.dll" - - foreach ($resource in $allPSResources) { - foreach ($inputResource in $inputObj.resources) { - if ($resource.Name -eq $inputResource.Name) { - if ($inputResource.Version) { - # Use the NuGet.Versioning package if available, otherwise do a simple comparison - try { - $versionRange = [NuGet.Versioning.VersionRange]::Parse($inputResource.Version) - $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($resource.Version.ToString()) - if ($versionRange.Satisfies($resourceVersion)) { - $resourcesExist += $resource - } - } - catch { - # Fallback: simple string comparison (not full NuGet range support) - if ($resource.Version.ToString() -eq $inputResource.Version) { - $resourcesExist += $resource - } - } - } - } - } - } +function TestPSResourceList { + param( + [PSCustomObject]$inputObj + ) - PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -inputResources $inputObj.resources -repositoryName $inputObj.repositoryName -scope $inputObj.Scope + $inputResources = ConvertInputToPSResource -inputObj $inputObj.resources -repositoryName $inputObj.repositoryName + $inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources) + + $currentState = GetPSResourceList -inputObj $inputObj + $inDesiredState = $currentState.IsInDesiredState($inputPSResourceList) + + if ($inDesiredState) { + Write-Trace -message "PSResourceList is in desired state." -level info + } + else { + Write-Trace -message "PSResourceList is NOT in desired state." -level warn + } + + $currentState._inDesiredState = $inDesiredState + + ## TODO confirm the output format + if ($inDesiredState) + { + $outputJson = [PSCustomObject]@{ + desiredState = '$desiredstate$' + actualState = '$currentState$' + inDesiredState = $inDesiredState + differingProperties = @() + } | ConvertTo-Json -Compress + + $outputJson = $outputJson -replace '\$desiredstate\$', $inputPSResourceList.ToJsonForTest() + $outputJson = $outputJson -replace '\$currentState\$', $currentState.ToJsonForTest() + } +} + +function TestOperation { + param( + [string]$ResourceType + ) + + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop + + switch ($ResourceType) { + 'repository' { throw [System.NotImplementedException]::new("Test operation is not implemented for RepositoryList resource.") } + 'repositorylist' { throw [System.NotImplementedException]::new("Test operation is not implemented for RepositoryList resource.") } + 'psresource' { throw [System.NotImplementedException]::new("Test operation is not implemented for PSResource resource.") } + 'psresourcelist' { + TestPSResourceList -inputObj $inputObj } default { throw "Unknown ResourceType: $ResourceType" } @@ -214,7 +434,7 @@ function ExportOperation { $_.Trusted, $_.Priority, $_.ApiVersion - ) | ConvertTo-Json -Compress + ).ToJson() } } @@ -222,25 +442,23 @@ function ExportOperation { 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } 'psresourcelist' { - $allPSResources = Get-PSResource - PopulatePSResourcesObject -allPSResources $allPSResources + $currentUserPSResources = Get-PSResource + $allUsersPSResources = Get-PSResource -Scope AllUsers + PopulatePSResourceListObject -allUsersPSResources $allUsersPSResources -currentUserPSResources $currentUserPSResources } default { throw "Unknown ResourceType: $ResourceType" } } } -function SetPSResources { +function SetPSResourceList { param( $inputObj ) - $repositoryName = $inputObj.repositoryName - $scope = $inputObj.scope - - if (-not $scope) { - $scope = 'CurrentUser' - } + $inputResources = ConvertInputToPSResource -inputObj $inputObj + $inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources) + $repositoryName = $inputObj.repositoryName $resourcesToUninstall = @() $resourcesToInstall = [System.Collections.Generic.Dictionary[string, psobject]]::new() @@ -314,7 +532,6 @@ function SetOperation { // diff == array of properties that are different // for other operations, DONOT return _inDesiredState - #> $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop @@ -351,7 +568,7 @@ function SetOperation { 'repositorylist' { throw [System.NotImplementedException]::new("Get operation is not implemented for RepositoryList resource.") } 'psresource' { throw [System.NotImplementedException]::new("Get operation is not implemented for PSResource resource.") } - 'psresourcelist' { return SetPSResources -inputObj $inputObj } + 'psresourcelist' { return SetPSResourceList -inputObj $inputObj } default { throw "Unknown ResourceType: $ResourceType" } } } @@ -361,6 +578,8 @@ function DeleteOperation { [string]$ResourceType ) + ## todo: delete for PSresourceList + $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop switch ($ResourceType) { 'repository' { @@ -387,102 +606,84 @@ function DeleteOperation { } } - -function FilterPSResourcesByRepository { - param ( - $allPSResources, - $repositoryName - ) - - if (-not $repositoryName) { - return $allPSResources - } - - $filteredResources = $allPSResources | Where-Object { $_.Repository -eq $repositoryName } - - return $filteredResources -} - -function PopulatePSResourcesObjectByRepository { +function PopulatePSResourceListObjectByRepository { param ( $resourcesExist, $inputResources, - $repositoryName, - $scope + $repositoryName ) $resources = @() - $resourcesObj = @() if (-not $resourcesExist) { - $resourcesObj = $inputResources | ForEach-Object { - [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exist = $false - } + $resources = $inputResources | ForEach-Object { + [PSResource]::new( + $_.Name + ) } } else { $resources += $resourcesExist | ForEach-Object { - [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exist = $true - } - } - - $resourcesObj = if ($scope) { - [pscustomobject]@{ - repositoryName = $repositoryName - scope = $scope - resources = $resources - } - } - else { - [pscustomobject]@{ - repositoryName = $repositoryName - resources = $resources - } + [PSResource]::new( + $_.Name, + $_.Version.PreRelease ? $_.Version.ToString() + "-" + $_.PreRelease : $_.Version.ToString(), + $_.Scope, + $_.RepositoryName, + $_.PreRelease ? $true : $false + ) } } - return ($resourcesObj | ConvertTo-Json -Compress) + $psresourceListObj = + [PSResourceList]::new( + $repositoryName, + $resources + ) + + return $psresourceListObj } -function PopulatePSResourcesObject { +function PopulatePSResourceListObject { param ( - $allPSResources + $allUsersPSResources, + $currentUserPSResources ) - $repoGrps = $allPSResources | Group-Object -Property Repository - - $repoGrps | ForEach-Object { - $repoName = $_.Name + $allPSResources = @() + $allPSResources += $allUsersPSResources | ForEach-Object { + return [PSResource]::new( + $_.Name, + $_.Version, + [Scope]::AllUsers, + $_.Repository, + $_.PreRelease ? $true : $false + ) + } - $resources = $_.Group | ForEach-Object { - [pscustomobject]@{ - name = $_.Name - version = $_.Version.ToString() - _exist = $true - } - } + $allPSResources += $currentUserPSResources | ForEach-Object { + return [PSResource]::new( + $_.Name, + $_.Version, + [Scope]::CurrentUser, + $_.Repository, + $_.PreRelease ? $true : $false + ) + } - $resourcesObj = [pscustomobject]@{ - repositoryName = $repoName - resources = $resources - } + $repoGrps = $allPSResources | Group-Object -Property repositoryName - $resourcesObj | ConvertTo-Json -Compress + $repoGrps | ForEach-Object { + $repoName = $_.Name + $resources = $_.Group + [PSResourceList]::new($repoName, $resources).ToJson() } } - - switch ($Operation.ToLower()) { 'get' { return (GetOperation -ResourceType $ResourceType) } 'set' { return (SetOperation -ResourceType $ResourceType) } + 'test' { return (TestOperation -ResourceType $ResourceType) } 'export' { return (ExportOperation -ResourceType $ResourceType) } 'delete' { return (DeleteOperation -ResourceType $ResourceType) } default { throw "Unknown Operation: $Operation" } diff --git a/src/dsc/psresourcelist.dsc.resource.json b/src/dsc/psresourcelist.dsc.resource.json index 68bd47f49..26808b1e3 100644 --- a/src/dsc/psresourcelist.dsc.resource.json +++ b/src/dsc/psresourcelist.dsc.resource.json @@ -16,7 +16,8 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation 'get'" ], @@ -28,7 +29,8 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set" ], @@ -41,7 +43,8 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "./psresourceget.ps1 -resourcetype 'psresourcelist' -operation export" ], @@ -53,7 +56,8 @@ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Bypass", + "-ExecutionPolicy", + "Bypass", "-Command", "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation test" ], @@ -66,26 +70,29 @@ "title": "PSResourceList", "type": "object", "additionalProperties": false, + "required": [ + "repositoryName" + ], "properties": { "repositoryName": { "title": "Repository Name", "description": "The name of the repository from where the resources are acquired.", "type": "string" }, - "scope": { - "title": "Scope", - "description": "The scope of the resources. Can be 'CurrentUser' or 'AllUsers'. Defaults to 'CurrentUser'.", - "$ref": "#/$defs/Scope", - "default": "CurrentUser", - "writeOnly": true - }, "resources": { "title": "Resources", "description": "The list of resources to manage.", "type": "array", "items": { "$ref": "#/$defs/PSResource" - } + }, + "minItems": 0 + }, + "_exist": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" + }, + "_inDesiredState": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" } }, "$defs": { @@ -139,8 +146,32 @@ "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" } } + }, + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json", + "title": "Instance should exist", + "description": "Indicates whether the DSC resource instance should exist.", + "type": "boolean", + "default": true, + "enum": [ + false, + true + ] + }, + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json", + "title": "Instance is in desired state", + "description": "Indicates whether the DSC resource instance is in the desired state.", + "type": "boolean", + "default": true, + "enum": [ + false, + true + ] } } } } -} \ No newline at end of file +}