Skip to content

Commit 864bda6

Browse files
committed
(MODULES-6697) Implement packageprovider type
This commit adds a new type and provider for managing the PowerShellGet package providers. **Note** The official API at this time does not allow removing PowerShellGet PackageProviders. There is a way to delete the source_location to remove a PackageProvider, but this is an unsupported method that may cause side effects. Thus this provider only implements create and update.
1 parent 7b5f096 commit 864bda6

File tree

7 files changed

+388
-6
lines changed

7 files changed

+388
-6
lines changed

README.md

+97-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [Use the PowerShell Gallery](#use-the-powershell-gallery)
1212
* [Side by side installation](#side-by-side-installation)
1313
* [The provider](#the-provider)
14+
* [Full working example](#full-working-example)
1415
1. [Reference](#reference)
1516
* [Types](#types)
1617
* [Providers](#providers)
@@ -31,16 +32,43 @@ For Windows PowerShell the PowerShellGet PowerShell module must be installed as
3132
the NuGet package provider. PowerShellGet is included with WMF5 or can be installed for earlier
3233
versions here http://go.microsoft.com/fwlink/?LinkID=746217&clcid=0x409
3334

34-
NuGet can be installed by running
35-
36-
`Install-PackageProvider Nuget –Force`
37-
3835
### PowerShell Core
3936

4037
PowerShellGet is included in PowerShell Core so no additional setup is necessary.
4138

4239
## Usage
4340

41+
### Install PowerShellGet PackageProviders
42+
43+
44+
You can install PackageProviders for PowerShelLGet using the `pspackageprovider` type.
45+
46+
```puppet
47+
pspackageprovider {'ExampleProvider':
48+
ensure => 'present',
49+
provider => 'windowspowershell',
50+
}
51+
```
52+
53+
In order to use this module to to get packages from a PSRepository like the `PSGallery`, you will have to ensure the `Nuget` provider is installed:
54+
55+
```puppet
56+
pspackageprovider {'Nuget':
57+
ensure => 'present',
58+
provider => 'windowspowershell',
59+
}
60+
```
61+
62+
You can optionally specify the version of a PackageProvider using the `version` parameter.
63+
64+
```puppet
65+
pspackageprovider {'Nuget':
66+
ensure => 'present',
67+
version => '2.8.5.208',
68+
provider => 'windowspowershell',
69+
}
70+
```
71+
4472
### Register an internal PowerShell repository
4573

4674
```puppet
@@ -102,15 +130,79 @@ package { 'PSExcel-psc':
102130

103131
The provider to use will either be `windowspowershell` or `powershellcore`. Nodes using `powershell.exe` will use `windowspowershell`, and nodes that have PowerShell core (`pwsh.exe`) will use the `powershellcore` provider with both the `psrepository` and `package` types.
104132

133+
### Full Working example
134+
135+
This complete example shows how to bootstrap the system with the Nuget package provider, ensure the PowerShell Gallery repository is configured and trusted, and install two modules (one using the WindowsPowerShell provider and one using the PowerShellCore provider).
136+
137+
```puppet
138+
pspackageprovider {'Nuget':
139+
ensure => 'present'
140+
}
141+
142+
psrepository { 'PSGallery':
143+
ensure => present,
144+
source_location => 'https://www.powershellgallery.com/api/v2/',
145+
installation_policy => 'trusted',
146+
}
147+
148+
package { 'xPSDesiredStateConfiguration':
149+
ensure => latest,
150+
provider => 'windowspowershell',
151+
source => 'PSGallery',
152+
}
153+
154+
package { 'Pester':
155+
ensure => latest,
156+
provider => 'powershellcore',
157+
source => 'PSGallery',
158+
}
159+
```
160+
105161
## Limitations
106162

107163
Note that PowerShell modules can be installed side by side so installing a newer
108164
version of a module will not remove any previous versions.
109165

166+
- As detailed in https://github.com/OneGet/oneget/issues/308, installing PackageProviders from a offline location instead of online is currently not working. A workaround is to use the Puppet file resource to ensure the prescence of the file before attempting to use the NuGet PackageProvider.
167+
168+
The following is an incompelete example that copies the NuGet provider dll to the directory that PowerShellGet expects. You would have to modify this declaration to complete the permissions for the target and the location of the source file.
169+
170+
```
171+
file{"C:\Program Files\PackageManagement\ProviderAssemblies\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll":
172+
ensure => 'file',
173+
source => "$source\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll"
174+
}
175+
176+
```
177+
110178
## Reference
111179

112180
### Types
113181

182+
* [package](#package)
183+
* [pspackageprovider](#pspackageprovider)
184+
* [psrepository](#psrepository)
185+
186+
### package
187+
188+
`puppet-powershellmodule` implements a [package type](http://docs.puppet.com/references/latest/type.html#package) with a resource provider, which is built into Puppet.
189+
190+
### pspackageprovider
191+
192+
#### Properties/Parameters
193+
194+
##### `ensure`
195+
196+
Specifies what state the PowerShellGet provider should be in. Valid options: `present` and `absent`. Default: `present`.
197+
198+
##### `name`
199+
200+
Specifies the name of the PowerShellGet provider to install.
201+
202+
##### `version`
203+
204+
Specifies the version of the PowerShellGet provider to install
205+
114206
### psrepository
115207

116208
Allows you to specify and configure a repository. The type expects a valid OneGet package provider source over an HTTP or HTTPS url.
@@ -140,4 +232,4 @@ The provider for systems that use PowerShell core via `pwsh.exe`.
140232

141233
## Development
142234

143-
https://github.com/hbuckle/puppet-powershellmodule
235+
https://github.com/hbuckle/puppet-powershellmodule

examples/init.pp

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
# Learn more about module testing here:
1010
# https://docs.puppet.com/guides/tests_smoke.html
1111
#
12+
pspackageprovider {'Nuget':
13+
ensure => 'present'
14+
}
15+
1216
psrepository { 'PSGallery':
1317
ensure => present,
1418
source_location => 'https://www.powershellgallery.com/api/v2/',
@@ -25,4 +29,4 @@
2529
ensure => latest,
2630
provider => 'powershellcore',
2731
source => 'PSGallery',
28-
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require 'json'
2+
3+
Puppet::Type.type(:pspackageprovider).provide :powershellcore do
4+
confine operatingsystem: :windows
5+
commands pwsh: 'pwsh'
6+
7+
mk_resource_methods
8+
9+
def self.invoke_ps_command(command)
10+
# override_locale is necessary otherwise the Install-Module commands silently fails on Linux
11+
result = Puppet::Util::Execution.execute(['pwsh', '-NoProfile', '-NonInteractive', '-NoLogo', '-Command',
12+
"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; #{command}"],
13+
override_locale: false)
14+
result.lines
15+
end
16+
17+
def initialize(value = {})
18+
super(value)
19+
@property_flush = {}
20+
end
21+
22+
def self.prefetch(resources)
23+
instances.each do |prov|
24+
if resource = resources[prov.name]
25+
resource.provider = prov
26+
end
27+
end
28+
end
29+
30+
def self.instances
31+
result = invoke_ps_command instances_command
32+
result.each.collect do |line|
33+
p = JSON.parse(line.strip, symbolize_names: true)
34+
p[:ensure] = :present
35+
new(p)
36+
end
37+
end
38+
39+
def exists?
40+
@property_hash[:ensure] == :present
41+
end
42+
43+
def create
44+
self.class.invoke_ps_command install_command
45+
@property_hash[:ensure] = :present
46+
end
47+
48+
def flush
49+
unless @property_flush.empty?
50+
flush_command = "PackageManagement\\Install-PackageProvider -Name #{@resource[:name]}"
51+
@property_flush.each do |key, value|
52+
if @property_flush[:version]
53+
flush_command << " -RequiredVersion '#{value}'"
54+
else
55+
flush_command << " -#{key} '#{value}'"
56+
end
57+
end
58+
flush_command < " -Force"
59+
self.class.invoke_ps_command flush_command
60+
end
61+
@property_hash = @resource.to_hash
62+
end
63+
64+
def self.instances_command
65+
<<-COMMAND
66+
@(Get-PackageProvider).foreach({
67+
[ordered]@{
68+
'name' = $_.Name.ToLower()
69+
'version' = $_.Version.ToString()
70+
} | ConvertTo-Json -Depth 99 -Compress
71+
})
72+
COMMAND
73+
end
74+
75+
def install_command
76+
command = []
77+
command << "PackageManagement\\Install-PackageProvider -Name #{@resource[:name]}"
78+
command << " -Force"
79+
command.join
80+
end
81+
82+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Puppet::Type.type(:pspackageprovider).provide(:windowspowershell, parent: :powershellcore) do
2+
confine operatingsystem: :windows
3+
commands powershell: 'powershell'
4+
5+
def self.invoke_ps_command(command)
6+
result = powershell(['-NoProfile', '-ExecutionPolicy', 'Bypass', '-NonInteractive', '-NoLogo', '-Command',
7+
"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; #{command}"])
8+
result.lines
9+
end
10+
end

lib/puppet/type/pspackageprovider.rb

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Puppet::Type.newtype(:pspackageprovider) do
2+
@doc = 'Manage PowerShell Package providers for PowerShell modules'
3+
4+
newproperty(:ensure) do
5+
newvalue(:present) do
6+
provider.create
7+
end
8+
end
9+
10+
newparam(:name, :namevar => true) do
11+
desc 'The name of the package provider'
12+
validate do |value|
13+
if value.nil? or value.empty?
14+
raise ArgumentError, "A non-empty #{self.name.to_s} must be specified."
15+
end
16+
fail "#{self.name.to_s} should be a String" unless value.is_a? ::String
17+
fail("#{value} is not a valid #{self.name.to_s}") unless value =~ /^[a-zA-Z0-9\.\-\_\'\s]+$/
18+
end
19+
munge(&:downcase)
20+
end
21+
22+
newproperty(:version) do
23+
desc 'The version for a PowerShell Package Provider'
24+
validate do |value|
25+
if value.nil? or value.empty?
26+
raise ArgumentError, "A non-empty #{self.name.to_s} must be specified."
27+
end
28+
fail "#{self.name.to_s} should be a String" unless value.is_a? ::String
29+
end
30+
end
31+
32+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'spec_helper'
2+
3+
provider_class = Puppet::Type.type(:pspackageprovider).provider(:windowspowershell)
4+
5+
describe provider_class do
6+
7+
before(:each) do
8+
type = Puppet::Type.type(:pspackageprovider).new(
9+
name: 'repo'
10+
)
11+
@provider_instance = provider_class.new(type)
12+
allow(provider_class).to receive(:invoke_ps_command).and_return(nil)
13+
allow(provider_class).to receive(:invoke_ps_command).with(
14+
provider_class.instances_command
15+
).and_return(
16+
[
17+
'{"name":"Repo1"}',
18+
'{"name":"Repo2"}'
19+
]
20+
)
21+
end
22+
23+
describe :instances do
24+
specify 'returns an array of :windowspowershell providers' do
25+
instances = provider_class.instances
26+
expect(instances.count).to eq(2)
27+
expect(instances).to all(be_instance_of(provider_class))
28+
end
29+
specify 'sets the property hash for each provider' do
30+
instances = provider_class.instances
31+
expect(instances[0].instance_variable_get('@property_hash')).to eq(
32+
name: 'Repo1', ensure: :present
33+
)
34+
expect(instances[1].instance_variable_get('@property_hash')).to eq(
35+
name: 'Repo2', ensure: :present
36+
)
37+
end
38+
end
39+
40+
describe :prefetch do
41+
specify 'sets the provider instance of the managed resource to a provider with the fetched state' do
42+
repo_resource1 = spy('pspackageprovider', name: 'Repo1')
43+
repo_resource2 = spy('pspackageprovider', name: 'Repo2')
44+
provider_class.prefetch(
45+
'Repo1' => repo_resource1,
46+
'Repo2' => repo_resource2
47+
)
48+
expect(repo_resource1).to have_received(:provider=).with(
49+
provider_class.instances[0]
50+
)
51+
expect(repo_resource2).to have_received(:provider=).with(
52+
provider_class.instances[1]
53+
)
54+
end
55+
end
56+
57+
describe :exists? do
58+
specify 'returns true if the resource already exists' do
59+
existing_instance = provider_class.instances[0]
60+
expect(existing_instance.exists?).to be true
61+
end
62+
specify 'returns false if the resource does not exist' do
63+
expect(@provider_instance.exists?).to be false
64+
end
65+
end
66+
67+
describe :create do
68+
specify 'calls install-packageprovider with parameters' do
69+
@provider_instance.create
70+
expect(provider_class).to have_received(:invoke_ps_command).with(
71+
"PackageManagement\\Install-PackageProvider -Name repo -Force"
72+
)
73+
end
74+
end
75+
76+
end

0 commit comments

Comments
 (0)