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

Add WhatIf support for DSC v3 #89

Merged
merged 14 commits into from
Nov 12, 2024
2 changes: 2 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ uilt
Windo
ELSPROBLEMS
requ
whatif
pscustomobject
12 changes: 6 additions & 6 deletions resources/PythonPip3Dsc/PythonPip3Dsc.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@
PSData = @{

# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('PSDscResource_Pip3Package')
Tags = @('PSDscResource_Pip3Package')

# A URL to the license for this module.
LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE'
LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE'

# A URL to the main website for this project.
ProjectUri = 'https://github.com/microsoft/winget-dsc'
ProjectUri = 'https://github.com/microsoft/winget-dsc'

# A URL to an icon representing this module.
# IconUri = ''
Expand All @@ -112,13 +112,14 @@
# ReleaseNotes = ''

# Prerelease string of this module
Prerelease = 'alpha'
Prerelease = 'alpha'

# Flag to indicate whether the module requires explicit user acceptance for install/update/save
# RequireLicenseAcceptance = $false

# External dependent modules of this module
# ExternalModuleDependencies = @()
DscCapabilities = @('Get', 'Set', 'Test', 'Export', 'WhatIf')

} # End of PSData hashtable

Expand All @@ -130,5 +131,4 @@
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}

}
111 changes: 97 additions & 14 deletions resources/PythonPip3Dsc/PythonPip3Dsc.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,61 @@
using namespace System.Collections.Generic

#region Functions
function Invoke-Process {
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$FilePath,

[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ArgumentList
)

try {
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $FilePath
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.WindowStyle = 'Hidden'
$pinfo.CreateNoWindow = $true
$pinfo.Arguments = $ArgumentList
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null

$stOut = @()
# using ReadLine() instead of ReadToEnd() for building array object. ReadToEnd() gave different output than ReadLine() in some cases.
while (-not $p.StandardOutput.EndOfStream) {
$stOut += $p.StandardOutput.ReadLine()
}

$stErr = @()
while (-not $p.StandardError.EndOfStream) {
$stErr += $p.StandardError.ReadLine()
}

$result = [pscustomobject]@{
Title = ($MyInvocation.MyCommand).Name
Command = $FilePath
Arguments = $ArgumentList
StdOut = $stOut
StdErr = $stErr
ExitCode = $p.ExitCode
}

$p.WaitForExit()

return $result
} catch {
Write-Verbose -Message "Error occurred while executing the command: $FilePath $ArgumentList. Error:"
Write-Verbose -Message $stErr
}
}

function Get-Pip3Path {
if ($IsWindows) {
# Note: When installing 64-bit version, the registry key: HKLM:\SOFTWARE\Wow6432Node\Python\PythonCore\*\InstallPath was empty.
Expand Down Expand Up @@ -77,13 +132,8 @@ function Get-PackageNameWithVersion {
[string]$PackageName,

[Parameter(Mandatory = $false)]
[string]$Arguments,

[Parameter(Mandatory = $false)]
[string]$Version,

[Parameter()]
[switch]$IsUpdate
[AllowNull()]
[string]$Version
)

if ($PSBoundParameters.ContainsKey('Version') -and -not ([string]::IsNullOrEmpty($Version))) {
Expand All @@ -105,17 +155,27 @@ function Invoke-Pip3Install {
[string]$Version,

[Parameter()]
[switch]$IsUpdate
[switch]$IsUpdate,

[Parameter()]
[switch]$DryRun
)

$command = [List[string]]::new()
$command.Add('install')
$command.Add((Get-PackageNameWithVersion @PSBoundParameters))
$command.Add((Get-PackageNameWithVersion -PackageName $PackageName -Version $Version))
if ($IsUpdate.IsPresent) {
$command.Add('--force-reinstall')
}
if ($DryRun.IsPresent) {
$command.Add('--dry-run')
}

$command.Add($Arguments)
return Invoke-Pip3 -command $command
Write-Verbose -Message "Executing 'pip' install with command: $command"
$result = Invoke-Pip3 -command $command

return $result
}

function Invoke-Pip3Uninstall {
Expand All @@ -132,10 +192,10 @@ function Invoke-Pip3Uninstall {

$command = [List[string]]::new()
$command.Add('uninstall')
$command.Add((Get-PackageNameWithVersion @PSBoundParameters))
$command.Add((Get-PackageNameWithVersion -PackageName $PackageName -Version $Version))
$command.Add($Arguments)

# '--yes' is needed to ignore confirmation required for uninstalls
# '--yes' is needed to ignore conformation required for uninstalls
Gijsreyn marked this conversation as resolved.
Show resolved Hide resolved
$command.Add('--yes')
return Invoke-Pip3 -command $command
}
Expand Down Expand Up @@ -211,9 +271,9 @@ function Invoke-Pip3 {
)

if ($global:usePip3Exe) {
return Start-Process -FilePath $global:pip3ExePath -ArgumentList $command -Wait -PassThru -WindowStyle Hidden
return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command
} else {
return Start-Process -FilePath pip3 -ArgumentList $command -Wait -PassThru -WindowStyle hidden
return Invoke-Process -FilePath pip3 -ArgumentList $command
}
}

Expand Down Expand Up @@ -339,6 +399,29 @@ class Pip3Package {
}
}

[string] WhatIf() {
if ($this.Exist) {
$whatIfState = Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments -DryRun

$whatIfResult = $whatIfState.StdOut
if ($whatIfState.ExitCode -ne 0) {
$whatIfResult = $whatIfState.StdErr
}

$out = @{
PackageName = $this.PackageName
_metaData = @{
whatIf = $whatIfResult
}
}
} else {
# Uninstall does not have --dry-run param
$out = @{}
}

return ($out | ConvertTo-Json -Depth 10 -Compress)
Gijsreyn marked this conversation as resolved.
Show resolved Hide resolved
}

static [Pip3Package[]] Export() {
$packages = GetInstalledPip3Packages
$out = [List[Pip3Package]]::new()
Expand Down
34 changes: 34 additions & 0 deletions tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,38 @@ Describe 'Pip3Package' {
$finalState = Invoke-DscResource -Name Pip3Package -ModuleName PythonPip3Dsc -Method Get -Property $desiredState
$finalState.Exist | Should -BeFalse
}

It 'Performs whatif operation successfully' -Skip:(!$IsWindows) {
$whatIfState = @{
PackageName = 'itsdangerous'
Version = '2.2.0'
Exist = $false
}

$pipPackage = [Pip3Package]$whatIfState

# Uninstall to make sure it is not present
$pipPackage.Set()

$pipPackage.Exist = $true

# Call whatif to see if it "will" install
$whatIf = $pipPackage.WhatIf() | ConvertFrom-Json

$whatIf.PackageName | Should -Be 'itsdangerous'
$whatIf._metaData.whatIf | Should -Contain "Would install itsdangerous-$($whatIfState.Version)"
}

It 'Does not return whatif result if package is invalid' -Skip:(!$IsWindows) {
$whatIfState = @{
PackageName = 'invalidPackageName'
}

$pipPackage = [Pip3Package]$whatIfState
$whatIf = $pipPackage.WhatIf() | ConvertFrom-Json


$whatIf.PackageName | Should -Be 'invalidPackageName'
$whatIf._metaData.whatIf | Should -Contain "ERROR: No matching distribution found for $($whatIfState.PackageName)"
}
}