PSPostMan.psm1
$Defaults = @{ NugetServerUrl = 'https://www.powershellgallery.com/api/v2/package/' LocalNuGetExePath = "$PSScriptRoot\nuget.exe" NuGetExeUrl = 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' } function New-Package { [OutputType([System.IO.FileInfo])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-not (Test-Path -Path $_ -PathType Container)) { throw "The folder '$_' does not exist." } else { $true } })] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [version]$Version, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Name = (Split-Path -Path $Path -Leaf), [Parameter()] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-not (Test-Path -Path $_ -PathType Container)) { throw "The folder '$_' does not exist." } else { $true } })] [string]$PackageFolderPath = '.', [Parameter()] [ValidateNotNullOrEmpty()] [string]$Authors, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Id, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Description, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Owners, [Parameter()] [ValidateNotNullOrEmpty()] [string]$LicenseUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ProjectUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$IconUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ReleaseNotes, [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$Tags, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-not (Compare-Object $_.Keys @('id', 'version'))) { throw 'One or more dependencies hashtables does not have the required keys: id and version.' } else { $true } })] [hashtable[]]$Dependencies, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$PassThru ) begin { $ErrorActionPreference = 'Stop' } process { try { if (($Version).Build -eq '-1') { $Version = "$Version.0" } #region Build the nuget spec $specParamNames = @( 'Version', 'Authors', 'Owners', 'LicenseUrl', 'ProjectUrl', 'IconUrl', 'ReleaseNotes', 'Tags', 'Dependencies' ) $tempSpecFilePath = "$env:TEMP\$((New-Guid).Guid).nuspec" $specParams = @{ Name = $Name FilePath = $tempSpecFilePath Force = $true } @($specParamNames).where({ $PSBoundParameters.ContainsKey($_) }).foreach({ $specParams[$_] = (Get-Variable -Name $_).Value }) $packSpec = New-PackageSpec @specParams #endregion ## Create the nuget package $null = Invoke-NuGet -Action 'pack' -Arguments @{ '-NoPackageAnalysis' = $null $packSpec.FullName = $null; OutputDirectory = $PackageFolderPath.TrimEnd('\') BasePath = $Path.TrimEnd('\') } if ($PassThru) { Get-Item -Path "$PackageFolderPath\$Name.$Version.nupkg" } } catch { $PSCmdlet.ThrowTerminatingError($_) } finally { Remove-Item -Path $tempSpecFilePath -ErrorAction Ignore } } } function Invoke-NuGet { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet('delete', 'list', 'pack', 'push')] [string]$Action, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$Arguments ) try { $argArr = @() $argArr += $Arguments.GetEnumerator() | Sort-Object Value | foreach { if (-not $_.Value) { '"{0}"' -f $_.Key } else { '-{0} "{1}"' -f $_.Key, $_.Value } } $argString = $argArr -join ' ' $stdOutTempFile = New-TemporaryFile $stdErrTempFile = New-TemporaryFile $startProcessParams = @{ FilePath = $Defaults.LocalNuGetExePath ArgumentList = "$Action $argString" RedirectStandardError = $stdErrTempFile.FullName RedirectStandardOutput = $stdOutTempFile.FullName Wait = $true PassThru = $true NoNewWindow = $true } $cmd = Start-Process @startProcessParams $cmdOutput = Get-Content -Path $stdOutTempFile.FullName -Raw $cmdError = Get-Content -Path $stdErrTempFile.FullName -Raw if ($cmd.ExitCode -ne 0) { throw $cmdError } else { Write-Verbose -Message $cmdOutput } } catch { $PSCmdlet.ThrowTerminatingError($_) } finally { Remove-Item -Path $stdOutTempFile.FullName, $stdErrTempFile.FullName -Force } } function New-PackageSpec { [OutputType([System.IO.FileInfo])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({ if ($_ -notmatch '\.nuspec$') { throw 'Invalid file path. Extension must be NUSPEC.' } else { $true } })] [string]$FilePath, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$Force, [Parameter()] [ValidateNotNullOrEmpty()] [version]$Version = '1.0.0', [Parameter()] [ValidateNotNullOrEmpty()] [string]$Authors = 'Adam Bertram', [Parameter()] [ValidateNotNullOrEmpty()] [string]$Id = $Name, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Description = $Name, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Owners, [Parameter()] [ValidateNotNullOrEmpty()] [string]$LicenseUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ProjectUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$IconUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ReleaseNotes, [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$Tags, [Parameter()] [ValidateNotNullOrEmpty()] [hashtable[]]$Dependencies ) begin { $ErrorActionPreference = 'Stop' } process { try { if ((Test-Path -Path $FilePath -PathType Leaf) -and (-not $Force.IsPresent)) { throw "The file [$($FilePath)] already exists and -Force was not used to overwrite." } [xml]$xDoc = @" <?xml version="1.0"?> <package> <metadata> <id>$Id</id> <version>$($Version.ToString())</version> <authors>$Authors</authors> <description>$Description</description> </metadata> </package> "@ $optionalNodes = @( "owners" "licenseUrl" "projectUrl" "iconUrl" "releaseNotes" "tags" "dependencies" ) @($optionalNodes).where({ $PSBoundParameters.ContainsKey($_) }).foreach({ if ($_ -eq 'Tags') { $nodeName = $_ -join ' ' } else { $nodeName = $_ } $nodeName = $nodeName $xNode = $xDoc.CreateElement($nodeName) if ($_ -eq 'Dependencies') { @($Dependencies).foreach({ $xDep = $xNode.AppendChild($xDoc.CreateElement('dependency')) $xDep.SetAttribute('id', $_.id) $xDep.SetAttribute('version', $_.version) $null = $xNode.AppendChild($xDep) }) } else { $xNode.InnerText = (Get-Variable -Name $_).Value } $null = $xDoc.package.metadata.AppendChild($xNode) }) $xDoc.Save($FilePath) Get-Item -Path $FilePath } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function Publish-Package { [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({ if ($_ -notmatch '\.nupkg$') { throw 'Invalid file path. Extension must be NUPKG.' } else { $true } })] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$FeedUrl, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ApiKey, [Parameter()] [ValidateNotNullOrEmpty()] [int]$Timeout ) begin { $ErrorActionPreference = 'Stop' } process { try { $nugetArgs = [ordered]@{ $Path = $null Timeout = $Timeout Source = $FeedUrl ApiKey = $ApiKey } $null = Invoke-NuGet -Action 'push' -Arguments $nugetArgs } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function Remove-Package { [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'NoPipeline')] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-not (Compare-Object $_.Keys @('Name', 'Version'))) { throw 'One or more hashtables in the Package parameter do not have Name/Version key/value pairs.' } else { $true } })] [hashtable]$PackageInfo, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Pipeline')] [ValidateNotNullOrEmpty()] [object]$Package, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias('Source')] [string]$FeedUrl = $Defaults.NugetServerUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NuGetApiKey = 'secret' ) begin { $ErrorActionPreference = 'Stop' } process { try { if ($PSBoundParameters.ContainsKey('Package')) { $pack = @{ Name = $Package.Name Version = $Package.Version } } elseif ($PSBoundParameters.ContainsKey('PackageInfo')) { $pack = @{ Name = $PackageInfo.Name Version = $PackageInfo.Version } } $nuGetArgs = @{ $pack.Name = $null $pack.Version = $null '-NonInteractive' = $null source = $FeedUrl } if ($PSBoundParameters.ContainsKey('NuGetApiKey')) { $nuGetArgs.ApiKey = $NuGetApiKey } Invoke-NuGet -Action 'delete' -Arguments $nuGetArgs } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function Get-DependentModule { [OutputType([System.Management.Automation.PSModuleInfo])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$ModuleName, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$Recurse ) begin { $ErrorActionPreference = 'Stop' } process { try { if ($depModuleNames = Get-Module -Name $ModuleName -ListAvailable | Select-Object -ExpandProperty RequiredModules) { $depModules = Get-Module -Name $depModuleNames -ListAvailable if ($Recurse.IsPresent) { Get-DependentModule -ModuleName $depModules.Name } else { $depModules } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function New-ModulePackage { [OutputType([System.IO.FileInfo])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$PassThru ) ## TODO ADB: Get all manifest attributes $moduleName = ($Path | Split-Path -Leaf) $manifest = Import-PowerShellDataFile -Path "$Path\$moduleName.psd1" $manifestAttribToPackageMap = @{ 'ModuleVersion' = 'Version' 'Description' = 'Description' 'Author' = 'Authors' @('PrivateData', 'PSData', 'Tags') = 'Tags' @('PrivateData', 'PSData', 'ProjectUri') = 'ProjectUrl' } $newPackageParams = @{ Name = $moduleName Path = $Path PackageFolderPath = $Path } if ($PassThru.IsPresent) { $newPackageParams.PassThru = $true } $manifestAttribToPackageMap.GetEnumerator() | foreach { $val = $manifest.Clone() if ($_.Key -is 'array') { foreach ($p in $_.Key) { $val = $val.$p } } else { $val = $manifest.($_.Key) } if ($val) { $newPackageParams.($_.Value) = $val } } New-PmPackage @newPackageParams } function Publish-Module { [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [string[]]$Name, [Parameter(Mandatory, ParameterSetName = 'ByPath')] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$NuGetApiKey, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FeedUrl = $Defaults.NugetServerUrl, [Parameter()] [ValidateNotNullOrEmpty()] [int]$Timeout, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$PublishDependentModules ) begin { $ErrorActionPreference = 'Stop' } process { try { if ($PSCmdlet.ParameterSetName -eq 'ByName') { $getModuleName = $Name $moduleName = $Name } else { $getModuleName = $Path $moduleName = Split-Path -Path $Path -Leaf } $modulesToPublish = Get-Module -Name $getModuleName -ListAvailable if (@($modulesToPublish).Count -ne @($moduleName).Count) { throw 'One or more modules could not be found.' } $publishPackParams = @{ FeedUrl = $FeedUrl ApiKey = $NuGetApiKey } if (($depModules = Get-DependentModule -ModuleName $moduleName -Recurse) -and (-not $PublishDependentModules.IsPresent)) { throw "The module(s) [$($moduleName -join ',')] have dependent module(s) [$($depModules.Name -join ',')]. Use -PublishDependentModules to publish these as well." } else { @($depModules).foreach({ if (-not (Test-ModuleExists -Name $_.Name)) { throw "The dependenent module [$($_.Name)] needs to be published but was not found." } else { Write-Verbose -Message "Creating package for module [$($_.Name)]..." $pkg = New-PmPackage -Path $_.ModuleBase -PassThru -Version $_.Version Publish-PmPackage @publishPackParams -Path $pkg.FullName Remove-Item -Path $pkg.FullName -ErrorAction Ignore } }) } @($modulesToPublish).foreach({ $newPkgParams = @{ Path = $_.ModuleBase PassThru = $true } if ($depModules) { $newPkgParams.Dependencies = @($depModules).foreach({ @{id=$_.Name; version=$_.Version} }) } $pkg = New-PmModulePackage @newPkgParams Publish-PmPackage @publishPackParams -Path $pkg.FullName }) } catch { $PSCmdlet.ThrowTerminatingError($_) } finally { Remove-Item -Path $pkg.FullName -ErrorAction Ignore } } } function Test-ModuleExists { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name ) begin { $ErrorActionPreference = 'Stop' } process { try { if (Get-Module -Name $Name -ListAvailable) { $true } else { $false } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function Find-Package { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$Name, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FeedUrl = $Defaults.NugetServerUrl ) begin { $ErrorActionPreference = 'Stop' } process { try { if ($PSBoundParameters.ContainsKey('Name')) { $whereFilter = { $_ -match "^$($Name -join '|')" } } else { $whereFilter = { $_ } } $nugetArgs = @{ Source = $FeedUrl } $packageList = Invoke-NuGet -Action 'list' -Arguments $nugetArgs if ($packageList -notmatch 'no packages found') { @($packageList).foreach({ $split = $_.Split(' ') $version = $split[-1] if ($split.Count -eq 2) { $packageName = $split[0] } else { $packageName = $split[0..-2] -join ' ' } [pscustomobject]@{Name = $packageName; Version = $version} }) } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function Publish-DscResource { ## TODO: Add pipeline support for Get-DscResource at some point [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-not (Get-DscResource -Name $_ -ErrorAction Ignore)) { throw "The DSC resource [$($_)] was not found" } else { $true } })] [string[]]$Name, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FeedUrl = $Defaults.NugetServerUrl, [Parameter()] [ValidateNotNullOrEmpty()] [string]$NuGetApiKey = 'secret', [Parameter()] [ValidateNotNullOrEmpty()] [switch]$PublishDependentModules ) begin { $ErrorActionPreference = 'Stop' } process { try { $publishPackParams = @{ FeedUrl = $FeedUrl } if ($NuGetApiKey) { $publishPackParams.ApiKey = $NuGetApiKey } ## TODO: Need to group these dependency checks together if multiple resources are passed so the same thing ## isn't done for every resource. Could also makes these parallel ## Ensure any and all dependent modules are available before proceeding @($Name).foreach({ $resourceName = $_ $resourceModule = Get-Module -Name (Get-DscResource -Name $resourceName).ModuleName -ListAvailable Write-Verbose -Message "The DSC resource [$($resourceName)] is in the module [$($resourceModule.Name)]" if ($dscModuleDeps = Get-DependentModule -ModuleName $resourceModule.Name) { Write-Verbose -Message "Found [$($dscModuleDeps.Count)] dependent module(s)..." $depModulesInFeed = Find-Package -Name $dscModuleDeps.Name @($dscModuleDeps).foreach({ if ($_.Name -notin $depModulesInFeed.Name) { if (-not $PublishDependentModules.IsPresent) { throw "The dependent module [$($_.Name)] is not published to the feed specified. Downloading this module will fail if uploaded now. Use -PublishDependentModules." } else { if (-not (Test-ModuleExists -Name $_.Name)) { throw "The dependenent module [$($_.Name)] needs to be published but was not found." } else { Publish-Module -FeedUrl $FeedUrl -Name $_.Name - Write-Verbose -Message "Creating package for module [$($_.Name)]..." $pkg = New-PmPackage -Path $_.ModuleBase -PassThru -Version $_.Version Publish-PmPackage @publishPackParams -Path $pkg.FullName Remove-Item -Path $pkg.FullName -ErrorAction Ignore } } } }) } $newPkgParams = @{ Path = $resourceModule.ModuleBase PassThru = $true Version = $resourceModule.Version Tags = "PsDscResource_$resourceName" ## Required for Find-DscResource to find the module } if ($dscModuleDeps) { $newPkgParams.Dependencies = @($dscModuleDeps).foreach({ @{id=$_.Name; version=$_.Version} }) } $pkg = New-PmPackage @newPkgParams Publish-PmPackage @publishPackParams -Path $pkg.FullName Remove-Item -Path $pkg.FullName -ErrorAction Ignore }) } catch { $PSCmdlet.ThrowTerminatingError($_) } } } |