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

(MODULES-6697) Implement packageprovider type and provider #9

Merged
merged 1 commit into from
Apr 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 97 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [Use the PowerShell Gallery](#use-the-powershell-gallery)
* [Side by side installation](#side-by-side-installation)
* [The provider](#the-provider)
* [Full working example](#full-working-example)
1. [Reference](#reference)
* [Types](#types)
* [Providers](#providers)
Expand All @@ -31,16 +32,43 @@ For Windows PowerShell the PowerShellGet PowerShell module must be installed as
the NuGet package provider. PowerShellGet is included with WMF5 or can be installed for earlier
versions here http://go.microsoft.com/fwlink/?LinkID=746217&clcid=0x409

NuGet can be installed by running

`Install-PackageProvider Nuget –Force`

### PowerShell Core

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

## Usage

### Install PowerShellGet PackageProviders


You can install PackageProviders for PowerShelLGet using the `pspackageprovider` type.

```puppet
pspackageprovider {'ExampleProvider':
ensure => 'present',
provider => 'windowspowershell',
}
```

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:

```puppet
pspackageprovider {'Nuget':
ensure => 'present',
provider => 'windowspowershell',
}
```

You can optionally specify the version of a PackageProvider using the `version` parameter.

```puppet
pspackageprovider {'Nuget':
ensure => 'present',
version => '2.8.5.208',
provider => 'windowspowershell',
}
```

### Register an internal PowerShell repository

```puppet
Expand Down Expand Up @@ -102,15 +130,79 @@ package { 'PSExcel-psc':

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.

### Full Working example

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).

```puppet
pspackageprovider {'Nuget':
ensure => 'present'
}

psrepository { 'PSGallery':
ensure => present,
source_location => 'https://www.powershellgallery.com/api/v2/',
installation_policy => 'trusted',
}

package { 'xPSDesiredStateConfiguration':
ensure => latest,
provider => 'windowspowershell',
source => 'PSGallery',
}

package { 'Pester':
ensure => latest,
provider => 'powershellcore',
source => 'PSGallery',
}
```

## Limitations

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

- 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.

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.

```
file{"C:\Program Files\PackageManagement\ProviderAssemblies\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll":
ensure => 'file',
source => "$source\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll"
}

```

## Reference

### Types

* [package](#package)
* [pspackageprovider](#pspackageprovider)
* [psrepository](#psrepository)

### package

`puppet-powershellmodule` implements a [package type](http://docs.puppet.com/references/latest/type.html#package) with a resource provider, which is built into Puppet.

### pspackageprovider

#### Properties/Parameters

##### `ensure`

Specifies what state the PowerShellGet provider should be in. Valid options: `present` and `absent`. Default: `present`.

##### `name`

Specifies the name of the PowerShellGet provider to install.

##### `version`

Specifies the version of the PowerShellGet provider to install

### psrepository

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

## Development

https://github.com/hbuckle/puppet-powershellmodule
https://github.com/hbuckle/puppet-powershellmodule
6 changes: 5 additions & 1 deletion examples/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
# Learn more about module testing here:
# https://docs.puppet.com/guides/tests_smoke.html
#
pspackageprovider {'Nuget':
ensure => 'present'
}

psrepository { 'PSGallery':
ensure => present,
source_location => 'https://www.powershellgallery.com/api/v2/',
Expand All @@ -25,4 +29,4 @@
ensure => latest,
provider => 'powershellcore',
source => 'PSGallery',
}
}
82 changes: 82 additions & 0 deletions lib/puppet/provider/pspackageprovider/powershellcore.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'json'

Puppet::Type.type(:pspackageprovider).provide :powershellcore do
confine operatingsystem: :windows
commands pwsh: 'pwsh'

mk_resource_methods

def self.invoke_ps_command(command)
# override_locale is necessary otherwise the Install-Module commands silently fails on Linux
result = Puppet::Util::Execution.execute(['pwsh', '-NoProfile', '-NonInteractive', '-NoLogo', '-Command',
"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; #{command}"],
override_locale: false)
result.lines
end

def initialize(value = {})
super(value)
@property_flush = {}
end

def self.prefetch(resources)
instances.each do |prov|
if resource = resources[prov.name]
resource.provider = prov
end
end
end

def self.instances
result = invoke_ps_command instances_command
result.each.collect do |line|
p = JSON.parse(line.strip, symbolize_names: true)
p[:ensure] = :present
new(p)
end
end

def exists?
@property_hash[:ensure] == :present
end

def create
self.class.invoke_ps_command install_command
@property_hash[:ensure] = :present
end

def flush
unless @property_flush.empty?
flush_command = "PackageManagement\\Install-PackageProvider -Name #{@resource[:name]}"
@property_flush.each do |key, value|
if @property_flush[:version]
flush_command << " -RequiredVersion '#{value}'"
else
flush_command << " -#{key} '#{value}'"
end
end
flush_command < " -Force"
self.class.invoke_ps_command flush_command
end
@property_hash = @resource.to_hash
end

def self.instances_command
<<-COMMAND
@(Get-PackageProvider).foreach({
[ordered]@{
'name' = $_.Name.ToLower()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the name is always ToLower I think you need a corresponding munge(&:downcase) in the type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Munge added

'version' = $_.Version.ToString()
} | ConvertTo-Json -Depth 99 -Compress
})
COMMAND
end

def install_command
command = []
command << "PackageManagement\\Install-PackageProvider -Name #{@resource[:name]}"
command << " -Force"
command.join
end

end
10 changes: 10 additions & 0 deletions lib/puppet/provider/pspackageprovider/windowspowershell.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Puppet::Type.type(:pspackageprovider).provide(:windowspowershell, parent: :powershellcore) do
confine operatingsystem: :windows
commands powershell: 'powershell'

def self.invoke_ps_command(command)
result = powershell(['-NoProfile', '-ExecutionPolicy', 'Bypass', '-NonInteractive', '-NoLogo', '-Command',
"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; #{command}"])
result.lines
end
end
32 changes: 32 additions & 0 deletions lib/puppet/type/pspackageprovider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Puppet::Type.newtype(:pspackageprovider) do
@doc = 'Manage PowerShell Package providers for PowerShell modules'

newproperty(:ensure) do
newvalue(:present) do
provider.create
end
end

newparam(:name, :namevar => true) do
desc 'The name of the package provider'
validate do |value|
if value.nil? or value.empty?
raise ArgumentError, "A non-empty #{self.name.to_s} must be specified."
end
fail "#{self.name.to_s} should be a String" unless value.is_a? ::String
fail("#{value} is not a valid #{self.name.to_s}") unless value =~ /^[a-zA-Z0-9\.\-\_\'\s]+$/
end
munge(&:downcase)
end

newproperty(:version) do
desc 'The version for a PowerShell Package Provider'
validate do |value|
if value.nil? or value.empty?
raise ArgumentError, "A non-empty #{self.name.to_s} must be specified."
end
fail "#{self.name.to_s} should be a String" unless value.is_a? ::String
end
end

end
76 changes: 76 additions & 0 deletions spec/unit/puppet/provider/pspackageprovider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'spec_helper'

provider_class = Puppet::Type.type(:pspackageprovider).provider(:windowspowershell)

describe provider_class do

before(:each) do
type = Puppet::Type.type(:pspackageprovider).new(
name: 'repo'
)
@provider_instance = provider_class.new(type)
allow(provider_class).to receive(:invoke_ps_command).and_return(nil)
allow(provider_class).to receive(:invoke_ps_command).with(
provider_class.instances_command
).and_return(
[
'{"name":"Repo1"}',
'{"name":"Repo2"}'
]
)
end

describe :instances do
specify 'returns an array of :windowspowershell providers' do
instances = provider_class.instances
expect(instances.count).to eq(2)
expect(instances).to all(be_instance_of(provider_class))
end
specify 'sets the property hash for each provider' do
instances = provider_class.instances
expect(instances[0].instance_variable_get('@property_hash')).to eq(
name: 'Repo1', ensure: :present
)
expect(instances[1].instance_variable_get('@property_hash')).to eq(
name: 'Repo2', ensure: :present
)
end
end

describe :prefetch do
specify 'sets the provider instance of the managed resource to a provider with the fetched state' do
repo_resource1 = spy('pspackageprovider', name: 'Repo1')
repo_resource2 = spy('pspackageprovider', name: 'Repo2')
provider_class.prefetch(
'Repo1' => repo_resource1,
'Repo2' => repo_resource2
)
expect(repo_resource1).to have_received(:provider=).with(
provider_class.instances[0]
)
expect(repo_resource2).to have_received(:provider=).with(
provider_class.instances[1]
)
end
end

describe :exists? do
specify 'returns true if the resource already exists' do
existing_instance = provider_class.instances[0]
expect(existing_instance.exists?).to be true
end
specify 'returns false if the resource does not exist' do
expect(@provider_instance.exists?).to be false
end
end

describe :create do
specify 'calls install-packageprovider with parameters' do
@provider_instance.create
expect(provider_class).to have_received(:invoke_ps_command).with(
"PackageManagement\\Install-PackageProvider -Name repo -Force"
)
end
end

end
Loading