AzureArtifactsPowerShellModuleHelper.psm1
<#
.SYNOPSIS Registers a PSRepository to the given Azure Artifacts feed if one does not already exist. .DESCRIPTION Registers a PSRepository to the given Azure Artifacts feed if one does not already exist. .EXAMPLE ``` PS C:\> [string] $repositoryName = Register-AzureArtifactsPSRepository -FeedUrl https://pkgs.dev.azure.com/YourOrganization/_packaging/YourFeed/nuget/v2 -RepositoryName 'MyAzureArtifacts' ``` Attempts to create a PSRepository to the given FeedUrl if one doesn't exist. If one does not exist, one will be created with the name `MyAzureArtifacts`. Since no Credential was provided, it will attempt to retrieve a PAT from the environmental variables. The name of the PSRepository to the FeedUrl is returned. ``` [System.Security.SecureString] $securePersonalAccessToken = 'YourPatGoesHere' | ConvertTo-SecureString -AsPlainText -Force [System.Management.Automation.PSCredential] $Credential = New-Object System.Management.Automation.PSCredential 'Username@DoesNotMatter.com', $securePersonalAccessToken [string] $feedUrl = 'https://pkgs.dev.azure.com/YourOrganization/_packaging/YourFeed/nuget/v2' [string] $repositoryName = Register-AzureArtifactsPSRepository -Credential $credential -FeedUrl $feedUrl ``` Attempts to create a PSRepository to the given FeedUrl if one doesn't exist, using the Credential provided. .INPUTS FeedUrl: (Required) The URL of the Azure Artifacts PowerShell feed to register. e.g. https://pkgs.dev.azure.com/YourOrganization/_packaging/YourFeed/nuget/v2. Note: PowerShell does not yet support the "/v3" endpoint, so use v2. RepositoryName: The name to use for the PSRepository if one must be created. If not provided, one will be generated. A PSRepository with the given name will only be created if one to the Feed URL does not already exist. Credential: The credential to use to connect to the Azure Artifacts feed. This should be created from a personal access token that has at least Read permissions to the Azure Artifacts feed. If not provided, the VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable will be checked, as per https://github.com/Microsoft/artifacts-credprovider#environment-variables. .OUTPUTS System.String Returns the Name of the PSRepository that can be used to connect to the given Feed URL. .NOTES If a Credential is not provided, it will attempt to retrieve a PAT from the environment variables, as per https://github.com/Microsoft/artifacts-credprovider#environment-variables This function writes to the error, warning, and information streams in different scenarios, as well as may throw exceptions for catastrophic errors. #> function Register-AzureArtifactsPSRepository { param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The URL of the Azure Artifacts PowerShell feed to register. e.g. https://pkgs.dev.azure.com/YourOrganization/_packaging/YourFeed/nuget/v2. Note: PowerShell does not yet support the "/v3" endpoint, so use v2.')] [ValidateNotNullOrEmpty()] [string] $FeedUrl, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The name to use for the PSRepository if one must be created. If not provided, one will be generated. A PSRepository with the given name will only be created if one to the Feed URL does not already exist.')] [string] $RepositoryName, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The credential to use to connect to the Azure Artifacts feed. This should be created from a personal access token that has at least Read permissions to the Azure Artifacts feed. If not provided, the VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable will be checked, as per https://github.com/Microsoft/artifacts-credprovider#environment-variables')] [System.Management.Automation.PSCredential] $Credential = $null ) Process { if ([string]::IsNullOrWhitespace($RepositoryName)) { [string] $organizationAndFeed = Get-AzureArtifactOrganizationAndFeedFromUrl -feedUrl $FeedUrl $RepositoryName = ('AzureArtifacts-' + $organizationAndFeed).TrimEnd('-') } $Credential = Get-AzureArtifactsCredential -credential $Credential Install-NuGetPackageProvider [string] $repositoryNameOfFeed = Register-AzureArtifactsPowerShellRepository -feedUrl $FeedUrl -repositoryName $RepositoryName -credential $Credential return $repositoryNameOfFeed } Begin { function Register-AzureArtifactsPowerShellRepository([string] $feedUrl, [string] $repositoryName, [System.Management.Automation.PSCredential] $credential) { $psRepositories = Get-PSRepository [PSCustomObject] $existingPsRepositoryForFeed = $psRepositories | Where-Object { $_.SourceLocation -ieq $feedUrl } [bool] $psRepositoryIsAlreadyRegistered = ($null -ne $existingPsRepositoryForFeed) if ($psRepositoryIsAlreadyRegistered) { return $existingPsRepositoryForFeed.Name } [PSCustomObject] $existingPsRepositoryWithSameName = $psRepositories | Where-Object { $_.Name -ieq $repositoryName } [bool] $psRepositoryWithDesiredNameAlreadyExists = ($null -ne $existingPsRepositoryWithSameName) if ($psRepositoryWithDesiredNameAlreadyExists) { $repositoryName += '-' + (Get-RandomCharacters -length 3) } if ($null -eq $credential) { [string] $computerName = $Env:ComputerName Write-Warning "Credentials were not provided, so we will attempt to register a new PSRepository to connect to '$feedUrl' on '$computerName' without credentials." Register-PSRepository -Name $repositoryName -SourceLocation $feedUrl -InstallationPolicy Trusted > $null } else { Register-PSRepository -Name $repositoryName -SourceLocation $feedUrl -InstallationPolicy Trusted -Credential $credential > $null } return $repositoryName } function Get-RandomCharacters([int] $length = 8) { [string] $word = (-join ((65..90) + (97..122) | Get-Random -Count $length | ForEach-Object { [char]$_ })) return $word } function Get-AzureArtifactOrganizationAndFeedFromUrl([string] $feedUrl) { # Azure Artifact feed URLs are of the format: 'https://pkgs.dev.azure.com/Organization/_packaging/Feed/nuget/v2' [bool] $urlMatchesRegex = $feedUrl -match 'https\:\/\/pkgs.dev.azure.com\/(?<Organization>.+?)\/_packaging\/(?<Feed>.+?)\/' if ($urlMatchesRegex) { return $Matches.Organization + '-' + $Matches.Feed } return [string]::Empty } function Install-NuGetPackageProvider { [bool] $nuGetPackageProviderIsNotInstalled = ($null -eq (Get-PackageProvider | Where-Object { $_.Name -ieq 'NuGet' })) if ($nuGetPackageProviderIsNotInstalled) { [string] $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name Write-Information 'Installing NuGet package provider for user '$currentUser'.' Install-PackageProvider NuGet -Scope CurrentUser -Force > $null } } } } <# .SYNOPSIS Install (if necessary) and import a module from the specified repository. .DESCRIPTION Install (if necessary) and import a module from the specified repository. .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Name: (Required) The name of the PowerShell module to install (if necessary) and import. Version: The specific version of the PowerShell module to install (if necessary) and import. If not provided, the latest version will be used. AllowPrerelease: If provided, prerelease versions are allowed to be installed and imported. This must be provided if specifying a Prerelease version in the Version parameter. RepositoryName: (Required) The name to use for the PSRepository that contains the module to import. This should be obtained from the Register-AzureArtifactsPSRepository cmdlet. Credential: The credential to use to connect to the Azure Artifacts feed. Force: If provided, the specified PowerShell module will always be downloaded and installed, even if the version is already installed. .OUTPUTS No outputs are returned. .NOTES If a Credential is not provided, it will attempt to retrieve a PAT from the environment variables, as per https://github.com/Microsoft/artifacts-credprovider#environment-variables This function writes to the error, warning, and information streams in different scenarios, as well as may throw exceptions for catastrophic errors. #> function Import-AzureArtifactsModule { [CmdletBinding(DefaultParameterSetName = 'PAT')] param ( [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'The name of the PowerShell module to install (if necessary) and import.')] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory = $false, HelpMessage = 'The specific version of the PowerShell module to install (if necessary) and import. If not provided, the latest version will be used.')] [string] $Version = $null, [Parameter(Mandatory = $false, HelpMessage = 'If provided, prerelease versions are allowed to be installed and imported. This must be provided if specifying a Prerelease version in the Version parameter.')] [switch] $AllowPrerelease = $false, [Parameter(Mandatory = $true, HelpMessage = 'The name to use for the PSRepository that contains the module to import. This should be obtained from the Register-AzureArtifactsPSRepository cmdlet.')] [string] $RepositoryName, [Parameter(Mandatory = $false, HelpMessage = 'The credential to use to connect to the Azure Artifacts feed. This should be created from a personal access token that has at least Read permissions to the Azure Artifacts feed. If not provided, the VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable will be checked, as per https://github.com/Microsoft/artifacts-credprovider#environment-variables')] [System.Management.Automation.PSCredential] $Credential = $null, [Parameter(Mandatory = $false, HelpMessage = 'If provided, the specified PowerShell module will always be downloaded and installed, even if the version is already installed.')] [switch] $Force = $false ) Process { $Credential = Get-AzureArtifactsCredential -credential $Credential if ($null -eq $credential) { [string] $computerName = $Env:ComputerName Write-Error "A personal access token was not found, so we cannot ensure a specific version (or the latest version) of PowerShell module '$Name' is installed on '$computerName'." } else { $Version = Install-ModuleVersion -powerShellModuleName $Name -versionToInstall $Version -allowPrerelease:$AllowPrerelease -repositoryName $RepositoryName -credential $credential -force:$Force } Import-ModuleVersion -powerShellModuleName $Name -version $Version } Begin { function Install-ModuleVersion([string] $powerShellModuleName, [string] $versionToInstall, [switch] $allowPrerelease, [string] $repositoryName, [System.Management.Automation.PSCredential] $credential, [switch] $force) { [string] $computerName = $Env:ComputerName [string[]] $currentModuleVersionsInstalled = (Get-Module -Name $powerShellModuleName -ListAvailable) | Select-Object -ExpandProperty 'Version' -Unique | Sort-Object -Descending [bool] $specificVersionWasRequestedAndIsAlreadyInstalled = ((![string]::IsNullOrWhitespace($versionToInstall)) -and $versionToInstall -in $currentModuleVersionsInstalled) if ($specificVersionWasRequestedAndIsAlreadyInstalled) { if (!$force) { return $versionToInstall } } [string] $existingLatestVersion = ($currentModuleVersionsInstalled | Select-Object -First 1) [bool] $moduleIsInstalledOnComputerAlready = ![string]::IsNullOrWhitespace($existingLatestVersion) [bool] $latestVersionShouldBeInstalled = [string]::IsNullOrWhitespace($versionToInstall) if ($latestVersionShouldBeInstalled) { [string] $latestModuleVersionAvailable = Get-LatestAvailableVersion -powerShellModuleName $powerShellModuleName -allowPrerelease:$allowPrerelease -repositoryName $repositoryName -credential $credential [bool] $moduleWasNotFoundInPsRepository = [string]::IsNullOrWhitespace($latestModuleVersionAvailable) if ($moduleWasNotFoundInPsRepository) { if ($moduleIsInstalledOnComputerAlready) { Write-Error "The PowerShell module '$powerShellModuleName' could not be found in the PSRepository '$repositoryName', so the latest version of the module could not be obtained. Perhaps the credentials used are not valid. The module version '$existingLatestVersion' is installed on computer '$computerName' though so it will be used." return $existingLatestVersion } else { throw "The PowerShell module '$powerShellModuleName' could not be found in the PSRepository '$repositoryName' so it cannot be downloaded and installed. Perhaps the credentials used are not valid. The module is not already installed on computer '$computerName', so it cannot be imported." } } $versionToInstall = $latestModuleVersionAvailable } else { [bool] $specifiedVersionDoesNotExist = ($null -eq (Find-Module -Name $powerShellModuleName -AllowPrerelease:$allowPrerelease -RequiredVersion $versionToInstall -Repository $repositoryName -Credential $credential -ErrorAction SilentlyContinue)) if ($specifiedVersionDoesNotExist) { if ($moduleIsInstalledOnComputerAlready) { Write-Error "The specified version '$versionToInstall' of PowerShell module '$powerShellModuleName' does not exist in the PSRepository '$repositoryName', so it cannot be installed on computer '$computerName'. Version '$existingLatestVersion' is already installed and will be imported instead." return $existingLatestVersion } else { [string] $latestModuleVersionAvailable = Get-LatestAvailableVersion -powerShellModuleName $powerShellModuleName -allowPrerelease:$allowPrerelease -repositoryName $repositoryName -credential $credential [bool] $moduleWasNotFoundInPsRepository = [string]::IsNullOrWhitespace($latestModuleVersionAvailable) if ($moduleWasNotFoundInPsRepository) { throw "The PowerShell module '$powerShellModuleName' could not be found in the PSRepository '$repositoryName' so it cannot be downloaded and installed. Perhaps the credentials used are not valid. The module is not already installed on computer '$computerName', so it cannot be imported." } Write-Error "The specified version '$versionToInstall' of PowerShell module '$powerShellModuleName' does not exist in the PSRepository '$repositoryName'. Version '$latestModuleVersionAvailable' will be installed instead." $versionToInstall = $latestModuleVersionAvailable } } } [bool] $versionNeedsToBeInstalled = ($versionToInstall -notin $currentModuleVersionsInstalled) -or $force if ($versionNeedsToBeInstalled) { [string] $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name [string] $moduleVersionsInstalledString = $currentModuleVersionsInstalled -join ',' Write-Information "Current installed versions of PowerShell module '$powerShellModuleName' on computer '$computerName' are '$moduleVersionsInstalledString'. Installing version '$versionToInstall' for user '$currentUser'." Install-Module -Name $powerShellModuleName -AllowPrerelease:$allowPrerelease -RequiredVersion $versionToInstall -Repository $repositoryName -Credential $credential -Scope CurrentUser -Force -AllowClobber } return $versionToInstall } function Get-LatestAvailableVersion([string] $powerShellModuleName, [switch] $allowPrerelease, [string] $repositoryName, [System.Management.Automation.PSCredential] $credential) { [string] $latestModuleVersionAvailable = Find-Module -Name $powerShellModuleName -AllowPrerelease:$allowPrerelease -Repository $repositoryName -Credential $credential -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'Version' -First 1 return $latestModuleVersionAvailable } function Import-ModuleVersion([string] $powerShellModuleName, [string] $version) { [bool] $isPrereleaseVersion = Test-PrereleaseVersion -version $version if (!$isPrereleaseVersion) { Import-Module -Name $powerShellModuleName -RequiredVersion $version -Global -Force } else { Import-ModulePrereleaseVersion -powerShellModuleName $powerShellModuleName -version $version } Write-ModuleVersionImported -powerShellModuleName $powerShellModuleName -version $version } function Test-PrereleaseVersion([string] $version) { [bool] $isPrereleaseVersion = $true [System.Version] $parsedVersion = $null if ([System.Version]::TryParse($Version, [ref]$parsedVersion)) { $isPrereleaseVersion = $false } return $isPrereleaseVersion } function Import-ModulePrereleaseVersion([string] $powerShellModuleName, [string] $version) { [string] $computerName = $Env:ComputerName # PowerShell is weird about the way it supports prerelease versions. # The directory it installs to and the version it gives it is just the version with the prerelease postfix removed. # So really Import-Module has no way of telling if a module is a stable version or a prerelease version. # So we need to strip off the prerelease portion of the version number (i.e. what comes after the hyphen) to # get the stable version number, which Import-Module will use to find it. [string] $prereleaseVersionsStablePortion = ($ValidModulePrereleaseVersionThatExists -split '-')[0] [bool] $stableVersionWasExtractedFromPrereleaseVersion = ($prereleaseVersionsStablePortion -ne $version) if ($stableVersionWasExtractedFromPrereleaseVersion) { Import-Module -Name $powerShellModuleName -RequiredVersion $prereleaseVersionsStablePortion -Global -Force } else { Write-Warning "The prerelease version '$version' of module '$powerShellModuleName' was requested to be imported on computer '$computerName', but we could not determine where it was installed to. The module will be imported without specifying the version to import." Import-Module -Name $powerShellModuleName -Global -Force } } function Write-ModuleVersionImported([string] $powerShellModuleName, [string] $version) { [string] $computerName = $Env:ComputerName $moduleImported = Get-Module -Name $powerShellModuleName [bool] $moduleWasImported = ($null -ne $moduleImported) if ($moduleWasImported) { [string] $moduleVersion = $moduleImported.Version Write-Information "Version '$moduleVersion' of module '$powerShellModuleName' was imported on computer '$computerName'." } else { Write-Error "The module '$powerShellModuleName' was not imported on computer '$computerName'." } } } } function Get-AzureArtifactsCredential([System.Management.Automation.PSCredential] $credential = $null) { if ($null -ne $credential) { return $credential } [System.Security.SecureString] $personalAccessToken = Get-SecurePersonalAccessTokenFromEnvironmentVariable if ($null -ne $personalAccessToken) { $credential = New-Object System.Management.Automation.PSCredential 'Username@DoesNotMatter.com', $personalAccessToken } return $credential } # Microsoft recommends storing the PAT in an environment variable: https://github.com/Microsoft/artifacts-credprovider#environment-variables function Get-SecurePersonalAccessTokenFromEnvironmentVariable { [System.Security.SecureString] $securePersonalAccessToken = $null [string] $personalAccessToken = [string]::Empty [string] $computerName = $Env:ComputerName [string] $patJsonValue = $Env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS if (![string]::IsNullOrWhiteSpace($patJsonValue)) { $patJson = ConvertFrom-Json $patJsonValue $personalAccessToken = $patJson.endpointCredentials.password if ([string]::IsNullOrWhitespace($personalAccessToken)) { Write-Warning "Found the environmental variable 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' on computer '$computerName', but could not retrieve the Personal Access Token from it." } else { $securePersonalAccessToken = ConvertTo-SecureString $personalAccessToken -AsPlainText -Force } } else { Write-Warning "Could not find the environment variable 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' on computer '$computerName' to extract the Personal Access Token from it." } return $securePersonalAccessToken } Export-ModuleMember -Function Import-AzureArtifactsModule Export-ModuleMember -Function Register-AzureArtifactsPSRepository |