Skip to content
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