In this blog post I’m going to demonstrate how you can use TFS2017’s Package feed functionality to host your own Powershell modules, how you can use PowershellGet to publish modules to this feed, and how to search, download and install modules from there.
Introduction
Since version 5, Powershell includes a package manager called PackageManagement, that allows you to search, download and install Powershell modules and scripts from a variety of sources in a unified manner.
Of these, the Powershell Gallery is probably the most well-known, but since it is backed (amongst others) by the NuGet infrastructure you can easily create and host your own package feed, for example to create a corporate script or module repository.
This is especially useful if you’ve written your own Cmdlets or functions that you use on multiple machines, while at the same time adding new functionality to them; By hosting these from your own package feed you have a single place to store and version your modules, and all it takes is one or two Powershell lines to update any machine with the latest version of your modules.
In fact, TFS2017 provides a way to easily create and host your own NuGet package feeds, which means its also perfectly suitable to store your own Powershell scripts and modules in. In this post, I’ll use this to host our own packages in so that they can be used from PowershellGet/PackageManagement.
I’m using Powershell 5.1 in combination with TFS2017. Some Cmdlets require elevated access, so be sure to run the Powershell ISE with Administrator privileges.
Installing the NuGet Package provider
Powershell PackageManagement (previously called “OneGet”) is actually a package aggregator (see the architecture explanation here) and is able to work with many different packaging sources. In order for PowershellGet to work with NuGet feeds, it needs the NuGet package provider. Installing it is as easy as:
Install-PackageProvider NuGet
In turn, it needs the NuGet client, so when installing the package provider it will offer to download and install the NuGet.exe if it is not already available. Alternatively, you can download the latest version
of NuGet from here and add its installation path to the Path environment variable so that the package provider can find it.
Updating the modules
Although Powershell 5 ships with the PowershellGet and PackageManagement modules, these are likely outdated and are missing important features (such as support for Register-PSRepository
‘s -Credential
argument).
The latest versions of these modules are hosted on the Powershell gallery, so we can use PowershellGet to update itself; by default Install-Module will get the latest available version of a module.
There is a ‘gotcha’ here: because the current versions of these modules were not installed using PowershellGet
, Install-Module
will refuse to remove the old versions of these modules. But by specifying the -Force
switch, you can tell it to perform a side-by-side installation, so that both the old and the new version are available. By default, the module is installed to %systemdrive%:\Program Files\WindowsPowerShell\Modules
:
Install-Module PackageManagement -Force Install-Module PowershellGet -Force
By the way, Install-Module
can tell the difference between pre-installed modules and the modules it has installed itself because in the latter, it adds a hidden PSGetModuleInfo.xml
file to the module directory.
Ensuring the correct versions are loaded
Note that there are now multiple versions of these modules available:
Get-Module -ListAvailable | Where-Object { $_.Name -match '(PowershellGet|PackageManagement)' }
Because of this, we unload any current versions of these modules, and reload the latest versions we just installed. Note that Powershell does not always load the module with the highest version, so you might want to explicitly specify the -Version
with Import-Module
and load PackageManagement first (PowershellGet depends on PackageManagement, which could cause the wrong version to be selected):
Remove-Module PowershellGet Remove-Module PackageManagement Import-Module PackageManagement -Version 1.1.3.0 Import-Module PowershellGet -Version 1.1.3.1
Get-Module
should now report these latest versions:
PS C:\> Get-Module ModuleType Version Name ExportedCommands ---------- ------- ---- ---------------- ... Script 1.1.3.0 PackageManagement {Find-Package, Find-PackageProvider, ... Script 1.1.3.1 PowershellGet {Find-Command, Find-DscResource, Find...
We are now ready to register a repository.
Creating a TFS2017 Package feed
Lets create a new Package feed to host our Powershell modules in. In TFS2017, under the Build & Release menu, select Packages and create a new feed, and ensure that you’re allowed to push packages to it:
Next, click on the “Connect to feed” button and copy the url that’s displayed there, it should be something along the lines of:
https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v3/index.json
However, as you can see this is a NuGet v3 url, whereas PowershellGet (currently) only supports v2 endpoints. Luckily, it is easy to construct the v2 endpoint from this, simply by replacing “v3/index.json
” with “v2
“:
https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2
Registering the feed
In order to use this feed with PowershellGet, it must be registered. Note that Register-PSRepository
requires two separate url arguments, a -SourceLocation
(where packages are retrieved from) and a -PublishLocation
(where new package versions should be uploaded to). For the TFS2017 package feed, we can use the same v2 endpoint for both.
Also, we should specify if the feed is Trusted
, or if we should provide confirmation every time a module is installed from this feed.
(N.B. If registering fails, check out the “The …is an invalid Web Uri error” section at the end of this post.)
$packageFeedUrl = "https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2" Register-PSRepository -Name "MyFeed" -SourceLocation $packageFeedUrl -PublishLocation $packageFeedUrl -InstallationPolicy Trusted
By default, the credentials of the user executing the script will also be used for authenticating with TFS. If you need a different set of credentials (maybe because you’re connecting from a different Active Directory domain), you can specify these with the -Credential
argument (if this argument is not recognized, you’re using an old version of PowershellGet):
$cred = Get-Credential -Message "Enter the credentials to access TFS with" -UserName "MyDomain\Leon" Register-PSRepository -Name "MyFeed" -SourceLocation $packageFeedUrl -PublishLocation $packageFeedUrl -InstallationPolicy Trusted -Credential $cred
If successful, Get-PSRepository
should report our own feed, next to the default PSGallery that we used to update the PowershellGet and PackageManagement modules with:
PS C:\> Get-PSRepository Name InstallationPolicy SourceLocation ---- ------------------ -------------- MyFeed Trusted https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2 PSGallery Untrusted https://www.powershellgallery.com/api/v2/
Uploading modules to the feed
Because powershell modules are already versioned (by the module’s .psd1
file) and are packaged by automatically PowershellGet, we can directly upload any module that is accessible via one of the directories in $env:PSModulePath
.
For demo purposes, if you don’t have a module handy, you can first download one from the PSGallery, e.g. the Invoke-MsBuild module:
Install-Module "Invoke-MsBuild" -Repository "PSGallery"
Now that we have our sample module, we can use Publish-Module
to upload it. Note that you need to specify to which feed you want to upload it, otherwise it will default to the PSGallery:
Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiKey VSTS
In case you’re wondering about the -NuGetApiKey VSTS
part: In the NuGet world, not everyone can ‘push’ (i.e. publish) packages to a NuGet feed, you need to specify a secret API key to do so. However, since we’re using a feed that is hosted by TFS2017, we instead specify the special value “VSTS
” as the API Key, and let TFS2017 use our Windows/Domain credentials to determine if we’re allowed to push to this feed.
N.B. Apparently Publish-Module
currently doesn’t handle the -Credential
argument correctly, so make sure that you’re executing this cmdlet as a recognised TFS user.
After this, the module should have been uploaded to our own package feed. You can see this on TFS’ Package page, but also by querying the feed from Powershell:
PS C:\> Find-Module -Repository "MyFeed" Version Name Repository Description ------- ---- ---------- ----------- 1.2.0 SampleModule MyFeed My sample module
Updating a module
Note that if we now try to push this same module again, it will fail:
PS C:\> Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiKey VSTS Publish-Module : The module 'SampleModule' with version '1.2.0' cannot be published as the current version '1.2.0' is already available in the repository 'https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2'. At line:1 char:1 + Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiK ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [Publish-Module], InvalidOperationException + FullyQualifiedErrorId : ModuleVersionIsAlreadyAvailableInTheGallery,Publish-Module
Clearly, this is by design. The reasoning behind this is that once a package has been made public this way other solutions may depend on it, so this specific version of the package may not change anymore, and therefore cannot be overwritten by pushing the same version again. Rather, if you want to make changes to the module, you should give it a new version number and push that instead.
Say we have made some changes to our local version of this module and we want to increment the version number to reflect this, we should modify:
1. The version number included in the module path (e.g. C:\Program Files\WindowsPowerShell\Modules\SampleModule\1.2.0
)
2. The ModuleVersion
value in the module manifest (i.e. the SampleModule.psd1
file in that directory)
Now the Publish-Module
will succeed, pushing the new version of the module to our package feed. If you run
Find-Module -Repository "MyFeed"
it may seem that only this new version is now available, but it’s still possible to acquire the older versions, as can be seen by:
PS C:\> Find-Module -Repository "MyFeed" -Name "SampleModule" -AllVersions Version Name Repository Description ------- ---- ---------- ----------- 1.2.1 SampleModule MyFeed My sample module 1.2.0 SampleModule MyFeed My sample module
As we’ve seen, by default Install-Module
will always take the highest available version of a module, but you can tell it to get a specific version as well:
Install-Module -Name "SampleModule" -Repository "MyFeed" -RequiredVersion "1.2.0"
The “…is an invalid Web Uri” error
During experimenting, I regularly encountered the following error:
Register-PSRepository : The specified Uri 'https://tfsserver/_packaging/MyPowershellModules/nuget/v2' for parameter 'SourceLocation' is an invalid Web Uri. Please ensure that it meets the Web Uri requirements.
Rather confusingly, I found that this could mean a variety of things:
– The specified url cannot be reached,
– The url it is not a valid NuGet v2 endpoint
– The specified -Credentials
are invalid, or
– The credentials are valid, but TFS2017 doesn’t grant them access.
Should you encounter this error, be sure to check all the above cases. I found that a good way to tell what’s going on is to do a HTTP request to the endpoint directly:
Invoke-WebRequest -Uri $packageFeedUrl -Credential $cred
Conclusion
Creating your own package feed and using it to distribute your modules with isn’t too difficult once you’ve seen a couple of the required Cmdlets in action. Enjoy 🙂
2 comments
Hey, I was stuck for a whole day on trying to use v3/index.json link. It was driving me crasy. Thanks for this article that is really complete!
Clebam
I received the invalid URI error! I followed your steps and tried.. to invoke…
Invoke-WebRequest : The underlying connection was closed: An unexpected error occurred on a receive.
In order to fix this, I had to run…
Add-Type @”
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
“@
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
Rachael Deja