Skip to content

WIP: DSC v3 resource for PSResourceGet #1852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doBuild.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
Expand Down
386 changes: 386 additions & 0 deletions src/dsc/psresourceget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
## Copyright (c) Microsoft Corporation. All rights reserved.
## Licensed under the MIT License.

[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
)
function Write-Trace {
param(
[string]$message,
[string]$level = 'Error'
)

$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)
}
}

# catch any un-caught exception and write it to the error stream
trap {
Write-Trace -Level Error -message $_.Exception.Message
exit 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future enhancement - specific exit codes for specific issues/exceptions.

}

function GetOperation {
param(
[string]$ResourceType
)

$inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably recommend defining a class for both the various input types and a function to handle both converting the input into the appropriate type and validating the input.

Right now reading $inputObj.<property> and trying to keep track is very difficult, plus we lose the IntelliSense/etc we would get from stronger typing.


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' { 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) {
Get-PSResource -Scope $inputObj.Scope
}
else {
Get-PSResource
}

if ($inputObj.repositoryName) {
$allPSResources = FilterPSResourcesByRepository -allPSResources $allPSResources -repositoryName $inputObj.repositoryName
}

$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
}
}
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seems very similar across different operations, can it be a helper function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are subtle differences, but I will see if I can refactor


PopulatePSResourcesObjectByRepository -resourcesExist $resourcesExist -inputResources $inputObj.resources -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' { 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
)

$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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be a loop like:

$properties = @('Name', 'Uri', 'Trusted', 'Priority')
for ($property in $properties) {
  if ($null -ne $inputObject.$property) {
    $splatt[$property] = $inputObject.$property
  }
}


if ($inputObj.repositoryType) {
$splatt['ApiVersion'] = $inputObj.repositoryType
}

if ($null -eq $rep) {
Register-PSResourceRepository @splatt
}
else {
Set-PSResourceRepository @splatt
}

return GetOperation -ResourceType $ResourceType
}

'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,
$repositoryName
)

if (-not $repositoryName) {
return $allPSResources
}

$filteredResources = $allPSResources | Where-Object { $_.Repository -eq $repositoryName }

return $filteredResources
}

function PopulatePSResourcesObjectByRepository {
param (
$resourcesExist,
$inputResources,
$repositoryName,
$scope
)

$resources = @()
$resourcesObj = @()

if (-not $resourcesExist) {
$resourcesObj = $inputResources | 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
}
}
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" }
}
Loading