PSResourceGet.Bootstrap.psm1
#Region '.\prefix.ps1' -1 using module .\Modules\DscResource.Base $script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common' Import-Module -Name $script:dscResourceCommonModulePath $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' #EndRegion '.\prefix.ps1' 7 #Region '.\Enum\SingleInstance.ps1' -1 enum SingleInstance { Yes } #EndRegion '.\Enum\SingleInstance.ps1' 5 #Region '.\Classes\001.PSResourceGetBootstrapReason.ps1' -1 <# .SYNOPSIS The reason a property of a DSC resource is not in desired state. .DESCRIPTION A DSC resource can have a read-only property `Reasons` that the compliance part (audit via Azure Policy) of Azure AutoManage Machine Configuration uses. The property Reasons holds an array of PSResourceGetBootstrapReason. Each PSResourceGetBootstrapReason explains why a property of a DSC resource is not in desired state. #> class PSResourceGetBootstrapReason { [DscProperty()] [System.String] $Code [DscProperty()] [System.String] $Phrase } #EndRegion '.\Classes\001.PSResourceGetBootstrapReason.ps1' 23 #Region '.\Classes\020.BootstrapPSResourceGet.ps1' -1 <# .SYNOPSIS The `BootstrapPSResourceGet` DSC resource is used to bootstrap the module Microsoft.PowerShell.PSResourceGet to the specified location. .DESCRIPTION The `BootstrapPSResourceGet` DSC resource is used to bootstrap the module Microsoft.PowerShell.PSResourceGet to the specified location. It supports two parameter sets: 'Destination' and 'Scope'. The 'Destination' parameter set allows you to specify a specific location to save the module, while the 'ModuleScope' parameter set saves the module to the appropriate `$env:PSModulePath` location based on the specified scope ('CurrentUser' or 'AllUsers'). The built-in parameter **PSDscRunAsCredential** can be used to run the resource as another user. ## Requirements * Target machine must be running a operating system supporting running class-based DSC resources. * Target machine must support running Microsoft.PowerShell.PSResourceGet. ## Known issues All issues are not listed here, see [here for all open issues](https://github.com/viscalyx/PSResourceGet.Bootstrap/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+BootstrapPSResourceGet). ### Property **Reasons** does not work with **PSDscRunAsCredential** When using the built-in parameter **PSDscRunAsCredential** the read-only property **Reasons** will return empty values for the properties **Code** and **Phrase. The built-in property **PSDscRunAsCredential** does not work together with class-based resources that using advanced type like the parameter **Reasons** have. .PARAMETER IsSingleInstance Specifies that only a single instance of the resource can exist in one and the same configuration. Must always be set to the value `Yes`. .PARAMETER Destination Specifies the destination path where the module should be saved. This parameter is mandatory when using the 'Destination' parameter set. The path must be a valid container. This parameter may not be used at the same time as the parameter `ModuleScope`. .PARAMETER ModuleScope Specifies the scope for saving the module. This parameter is used when using the 'ModuleScope' parameter set. The valid values are 'CurrentUser' and 'AllUsers'. The default value is 'CurrentUser'. This parameter may not be used at the same time as the parameter Destination. .PARAMETER Version Specifies the version of the Microsoft.PowerShell.PSResourceGet module to download. If not specified, the latest version will be downloaded. .EXAMPLE Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{ IsSingleInstance = 'Yes' ModuleScope = 'CurrentUser' } This example shows how to call the resource using Invoke-DscResource. This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it to the appropriate location based on the scope `'CurrentUser'`. .EXAMPLE Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{ IsSingleInstance = 'Yes' ModuleScope = 'CurrentUser' Version = '1.0.2' } This example shows how to call the resource using Invoke-DscResource. This example bootstraps the Microsoft.PowerShell.PSResourceGet module with version 1.0.2, saving it to the appropriate location based on the scope `'CurrentUser'`. .EXAMPLE Invoke-DscResource -ModuleName PSResourceGet.Bootstrap -Name BootstrapPSResourceGet -Method Get -Property @{ IsSingleInstance = 'Yes' Destination = '/path/to/destination' } This example shows how to call the resource using Invoke-DscResource. This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it to the path specified in the parameter `Destination`. #> [DscResource(RunAsCredential = 'Optional')] class BootstrapPSResourceGet : ResourceBase { [DscProperty(Key)] [SingleInstance] $IsSingleInstance # The Destination is evaluated if exist in AssertProperties(). [DscProperty()] [System.String] $Destination <# The ModuleScope is evaluated in AssertProperties(). This parameter cannot use the ValidateSet() attribute since it is not possible to set a null value, unless it is set to [ValidateSet('CurrentUser', 'AllUsers', $null)] . The parameter name Scope could not be used as it is a reserved keyword in PowerShell DSC, if used it throws an error when parsing a configuration. #> [DscProperty()] [System.String] $ModuleScope # The Version is evaluated if exist in AssertProperties(). [DscProperty()] [System.String] $Version [DscProperty(NotConfigurable)] [PSResourceGetBootstrapReason[]] $Reasons BootstrapPSResourceGet () : base ($PSScriptRoot) { # These properties will not be enforced. $this.ExcludeDscProperties = @( 'IsSingleInstance' ) } [BootstrapPSResourceGet] Get() { # Call the base method to return the properties. return ([ResourceBase] $this).Get() } [System.Boolean] Test() { # Call the base method to test all of the properties that should be enforced. return ([ResourceBase] $this).Test() } [void] Set() { # Call the base method to enforce the properties. ([ResourceBase] $this).Set() } <# Base method Get() call this method to get the current state as a hashtable. The parameter keyProperty will contain the key properties. #> hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $keyProperty) { Write-Debug -Message ( 'Enter GetCurrentState. Parameters: {0}' -f ($keyProperty | ConvertTo-Json -Compress) ) Write-Verbose -Message $this.localizedData.EvaluateModule $currentState = @{ IsSingleInstance = [SingleInstance]::Yes } # Need to find out how to evaluate state since there are no key properties for that. $assignedDscProperties = $this | Get-DscProperty -HasValue -Attribute @( 'Mandatory' 'Optional' ) $testModuleExistParameters = @{ Name = 'Microsoft.PowerShell.PSResourceGet' } if ($assignedDscProperties.Keys -contains 'Version') { $testModuleExistParameters.Version = $assignedDscProperties.Version $currentState.Version = $null } # If it is ModuleScope wasn't specified, then destination was specified. if ($assignedDscProperties.Keys -contains 'ModuleScope') { Write-Verbose -Message ( $this.localizedData.EvaluatingScope -f $assignedDscProperties.ModuleScope ) $currentState.ModuleScope = $null $testModuleExistParameters.Scope = $assignedDscProperties.ModuleScope if ((Test-ModuleExist @testModuleExistParameters -ErrorAction 'Stop')) { $currentState.ModuleScope = $assignedDscProperties.ModuleScope if ($assignedDscProperties.Keys -contains 'Version') { $currentState.Version = $assignedDscProperties.Version } } } else { Write-Verbose -Message ( $this.localizedData.EvaluatingDestination -f $assignedDscProperties.Destination ) $currentState.Destination = $null $testModuleExistParameters.Path = $assignedDscProperties.Destination if ((Test-ModuleExist @testModuleExistParameters -ErrorAction 'Stop')) { $currentState.Destination = $assignedDscProperties.Destination if ($assignedDscProperties.Keys -contains 'Version') { $currentState.Version = $assignedDscProperties.Version } } } Write-Debug -Message 'Exit GetCurrentState' return $currentState } <# Base method Set() call this method with the properties that should be enforced are not in desired state. It is not called if all properties are in desired state. The variable $property contain the properties that are not in desired state. #> hidden [void] Modify([System.Collections.Hashtable] $property) { Write-Debug -Message ( 'Enter Modify. Parameters: {0}' -f ($property | ConvertTo-Json -Compress) ) Write-Verbose -Message $this.localizedData.Bootstrapping if ($property.Keys -contains 'ModuleScope') { $property.Scope = $property.ModuleScope $property.Remove('ModuleScope') } Write-Debug -Message "Start-PSResourceGetBootstrap Parameters:`n$($property | Out-String)" Start-PSResourceGetBootstrap @property -Force -ErrorAction 'Stop' Write-Debug -Message 'Exit Modify' } <# Base method Assert() call this method with the properties that was assigned a value. #> hidden [void] AssertProperties([System.Collections.Hashtable] $property) { Write-Debug -Message ( 'Enter AssertProperties. Parameters: {0}' -f ($property | ConvertTo-Json -Compress) ) # The properties ModuleScope and Destination are mutually exclusive. $assertBoundParameterParameters = @{ BoundParameterList = $property MutuallyExclusiveList1 = @( 'ModuleScope' ) MutuallyExclusiveList2 = @( 'Destination' ) } Assert-BoundParameter @assertBoundParameterParameters if ($property.Keys -notcontains 'ModuleScope' -and $property.Keys -notcontains 'Destination') { $errorMessage = $this.localizedData.MissingRequiredParameter New-InvalidArgumentException -ArgumentName 'ModuleScope, Destination' -Message $errorMessage } if ($property.Keys -contains 'ModuleScope') { <# It is not possible to set a null value to the parameter ModuleScope when it has a [ValidateSet()] unless it would be set to [ValidateSet('CurrentUser', 'AllUsers', $null)]. But that would give a strange output if giving the wrong value to the parameter: E.g. 'The argument "CurrentUser2" does not belong to the set "CurrentUser,AllUsers," specified by the ValidateSet attribute.' #> if ($property.ModuleScope -notin ('CurrentUser', 'AllUsers')) { $errorMessage = $this.localizedData.ModuleScopeInvalid -f $property.ModuleScope New-InvalidArgumentException -ArgumentName 'ModuleScope' -Message $errorMessage } $scopeModulePath = Get-PSModulePath -Scope $property.ModuleScope if ([System.String]::IsNullOrEmpty($scopeModulePath) -or -not (Test-Path -Path $scopeModulePath)) { $errorMessage = $this.localizedData.ScopePathInvalid -f $property.ModuleScope, $scopeModulePath New-InvalidArgumentException -ArgumentName 'ModuleScope' -Message $errorMessage } } if ($property.Keys -contains 'Destination') { if ([System.String]::IsNullOrEmpty($property.Destination) -or -not (Test-Path -Path $property.Destination)) { $errorMessage = $this.localizedData.DestinationInvalid -f $property.Destination New-InvalidArgumentException -ArgumentName 'Destination' -Message $errorMessage } } if ($property.Keys -contains 'Version') { $isValidVersion = ( # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string $property.Version -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or # Need to support the Nuget range syntax as well. $property.Version -match '^[\[(][0-9\.\,]*[\])]$' ) if (-not $isValidVersion) { $errorMessage = $this.localizedData.VersionInvalid -f $property.Version New-InvalidArgumentException -ArgumentName 'Version' -Message $errorMessage } } Write-Debug -Message 'Exit AssertProperties' } } #EndRegion '.\Classes\020.BootstrapPSResourceGet.ps1' 345 #Region '.\Public\Start-PSResourceGetBootstrap.ps1' -1 <# .SYNOPSIS Bootstraps the Microsoft.PowerShell.PSResourceGet module to the specified location. .DESCRIPTION The command Start-PSResourceGetBootstrap is used to bootstrap the Microsoft.PowerShell.PSResourceGet module. It supports two parameter sets: 'Destination' and 'Scope'. The 'Destination' parameter set allows you to specify a specific location to save the module, while the 'Scope' parameter set saves the module to the appropriate `$env:PSModulePath` location based on the specified scope ('CurrentUser' or 'AllUsers'). .PARAMETER Destination Specifies the destination path where the module should be saved. This parameter is mandatory when using the 'Destination' parameter set. The path must be a valid container. .PARAMETER Scope Specifies the scope for saving the module. This parameter is used when using the 'Scope' parameter set. The valid values are 'CurrentUser' and 'AllUsers'. The default value is 'CurrentUser'. .PARAMETER Version Specifies the version of the Microsoft.PowerShell.PSResourceGet module to download. If not specified, the latest version will be downloaded. .PARAMETER UseCompatibilityModule Indicates whether to use the compatibility module. If this switch parameter is present, the compatibility module will be downloaded. .PARAMETER CompatibilityModuleVersion Specifies the version of the compatibility module to download. If not specified, it will default to a minimum required range that includes previews. .PARAMETER Force Forces the operation without prompting for confirmation. This is useful when running the script in non-interactive mode. .PARAMETER ImportModule Indicates whether to import the module after it has been downloaded. .OUTPUTS None. .EXAMPLE Start-PSResourceGetBootstrap -Destination 'C:\Modules' This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it to the specified destination path "C:\Modules". .EXAMPLE Start-PSResourceGetBootstrap -Scope 'AllUsers' This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it to the appropriate location based on the 'AllUsers' scope. .EXAMPLE Start-PSResourceGetBootstrap -UseCompatibilityModule This example bootstraps the Microsoft.PowerShell.PSResourceGet module, saving it to the appropriate location based on the default scope ('CurrentUser'). It will also save the compatibility module to the same location. #> function Start-PSResourceGetBootstrap { # TODO: Change impact to 'Medium' when the script is stable. [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Scope')] [OutputType()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Destination')] [ValidateScript({ Write-Verbose -Message "Destination folder is set to '$($_)'." Test-Path -Path $_ -PathType 'Container' })] [System.String] $Destination, [Parameter(ParameterSetName = 'Scope')] [ValidateSet('CurrentUser', 'AllUsers')] [System.String] $Scope = 'CurrentUser', [Parameter()] [ValidateScript({ # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string $_ -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or # Need to support the Nuget range syntax as well. $_ -match '^[\[(][0-9\.\,]*[\])]$' })] [System.String] $Version, [Parameter()] [System.Management.Automation.SwitchParameter] $UseCompatibilityModule, [Parameter()] [ValidateScript({ # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string $_ -match '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' -or # Need to support the Nuget range syntax as well. $_ -match '^[\[(][0-9\.\,]*[\])]$' })] [System.String] $CompatibilityModuleVersion = '[3.0.22,]', [Parameter()] [System.Management.Automation.SwitchParameter] $ImportModule, [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) if ($Force.IsPresent -and -not $Confirm) { $ConfirmPreference = 'None' } $name = 'Microsoft.PowerShell.PSResourceGet' switch ($PSCmdlet.ParameterSetName) { 'Destination' { # Resolve relative path to absolute path. $Destination = Resolve-Path -Path $Destination -ErrorAction 'Stop' $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_Destination_ShouldProcessVerboseDescription -f $name, $Destination Write-Debug -Message ($script:localizedData.Start_PSResourceGetBootstrap_Destination_SaveModule -f $Destination) } 'Scope' { $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_Scope_ShouldProcessVerboseDescription -f $name, $Scope $scopeModulePath = Get-PSModulePath -Scope $Scope if (-not (Test-Path -Path $scopeModulePath)) { # cSpell: ignore SPSRGB $exception = New-Exception -Message ($script:localizedData.Start_PSResourceGetBootstrap_MissingScopePath -f $scopeModulePath, $Scope) $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0004' -ErrorCategory 'InvalidOperation' -TargetObject $name $PSCmdlet.ThrowTerminatingError($errorRecord) } $Destination = $scopeModulePath Write-Debug -Message ($script:localizedData.Start_PSResourceGetBootstrap_Scope_SaveModule -f $Scope, $Destination) } } $loadedModule = Get-Module -Name $name if ($loadedModule -and $loadedModule.Path -match [System.Text.RegularExpressions.Regex]::Escape($Destination)) { Write-Verbose -Message ($script:localizedData.Start_PSResourceGetBootstrap_AlreadyInUse -f $name) # Since it is loaded into the session, assume it is downloaded and working. $moduleAvailable = $true } else { $verboseWarningMessage = $script:localizedData.Start_PSResourceGetBootstrap_ShouldProcessVerboseWarning -f $name $captionMessage = $script:localizedData.Start_PSResourceGetBootstrap_ShouldProcessCaption -f $name if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) { $moduleAvailable = $false try { if (-not $Version) { # Default to latest version if no version is passed in parameter or specified in configuration. $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$name" } else { $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$name/$Version" } $invokeWebRequestParameters = @{ # TODO: Should support proxy parameters passed to the command. Uri = $psResourceGetUri OutFile = "$Destination/$name.nupkg" # cSpell: ignore nupkg ErrorAction = 'Stop' } Invoke-WebRequest @invokeWebRequestParameters $moduleAvailable = $true } catch { $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_FailedDownload -f $name) $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0001' -ErrorCategory 'InvalidOperation' -TargetObject $name $PSCmdlet.ThrowTerminatingError($errorRecord) } if ($moduleAvailable) { try { # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension. $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip') $renameItemParameters = @{ Path = $invokeWebRequestParameters.OutFile NewName = $zipFileName Force = $true } Rename-Item @renameItemParameters } catch { # If the rename fails, we should remove the .nupkg file. Remove-Item -Path $invokeWebRequestParameters.OutFile $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_RenamedFailed -f $invokeWebRequestParameters.OutFile, $invokeWebRequestParameters.NewName) $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0002' -ErrorCategory 'InvalidOperation' -TargetObject $invokeWebRequestParameters.OutFile $PSCmdlet.ThrowTerminatingError($errorRecord) } try { $zipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName $expandArchiveParameters = @{ Path = $zipArchivePath DestinationPath = "$Destination/$name" Force = $true } Expand-Archive @expandArchiveParameters } catch { $exception = New-Exception -ErrorRecord $_ -Message ($script:localizedData.Start_PSResourceGetBootstrap_ExpandFailed -f $zipArchivePath) $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SPSRGB0003' -ErrorCategory 'InvalidOperation' -TargetObject $zipArchivePath $PSCmdlet.ThrowTerminatingError($errorRecord) } finally { # When the expand succeeds, we should remove the .zip file. Remove-Item -Path $zipArchivePath } if ($ImportModule.IsPresent) { Import-Module -Name $expandArchiveParameters.DestinationPath -Force } } } else { if ((Test-Path -Path (Join-Path -Path $Destination -ChildPath $name))) { # Since it is available in the destination, assume it is downloaded and can work. $moduleAvailable = $true Write-Debug -Message 'Did not bootstrap, but module is available in the destination.' } } } if ($moduleAvailable -and $UseCompatibilityModule.IsPresent) { $name = 'PowerShellGet' $loadedModule = Get-Module -Name $name if ($loadedModule -and $loadedModule.Path -match [System.Text.RegularExpressions.Regex]::Escape($Destination)) { Write-Verbose -Message ($script:localizedData.Start_PSResourceGetBootstrap_AlreadyInUse -f $name) } else { $verboseDescriptionMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessVerboseDescription -f $name, $Destination $verboseWarningMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessVerboseWarning -f $name $captionMessage = $script:localizedData.Start_PSResourceGetBootstrap_CompatibilityModule_ShouldProcessCaption -f $name if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) { $savePowerShellGetParameters = @{ Name = $name Path = $Destination Repository = 'PSGallery' TrustRepository = $true # If not specified, default to a minimum required range that includes previews. Version = $CompatibilityModuleVersion # TODO: Should probably be a switch parameter when there is a full release out. Prerelease = $true } Save-PSResource @savePowerShellGetParameters if ($ImportModule.IsPresent) { Import-Module -Name "$Destination/$name" } } } } } #EndRegion '.\Public\Start-PSResourceGetBootstrap.ps1' 316 |