Skip to content

Add logic to correctly switch between channels; Determine channel from ensure #25

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

Merged
merged 2 commits into from
Sep 12, 2024
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
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ To install from specific channel:

```puppet
package { 'hello-world':
ensure => installed,
provider => 'snap',
install_options => ['channel=beta'],
ensure => 'beta',
provider => 'snap',
}
```

Expand All @@ -104,9 +103,19 @@ package { 'hello-world':
install_options => ['classic'],
}
```

Same applies for options `jailmode` and `devmode`

This snippet
```puppet
package { 'hello-world':
ensure => latest,
provider => 'snap',
install_options => ['classic'],
}
```

installs by default the `latest/stable` channel

## Reference

See [REFERENCE](https://github.com/root-expert/puppet-snap/blob/master/REFERENCE.md)
Expand Down
118 changes: 59 additions & 59 deletions lib/puppet/provider/package/snap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,109 +7,109 @@
desc "Package management via Snap.

This provider supports the `install_options` attribute, which allows snap's flags to be
passed to Snap. Namely `classic`, `dangerous`, `devmode`, `jailmode`, `channel`."
passed to Snap. Namely `classic`, `dangerous`, `devmode`, `jailmode`.

The 'channel' install option is deprecated and will be removed in a future release.
"

commands snap_cmd: '/usr/bin/snap'
has_feature :installable, :install_options, :uninstallable, :purgeable
has_feature :installable, :versionable, :install_options, :uninstallable, :purgeable, :upgradeable
confine feature: %i[net_http_unix_lib snapd_socket]

def self.instances
instances = []
snaps = installed_snaps
mk_resource_methods

snaps.each do |snap|
instances << new(name: snap['name'], ensure: snap['version'], provider: 'snap')
def self.instances
installed_snaps.map do |snap|
new(name: snap['name'], ensure: snap['tracking-channel'], provider: 'snap')
end

instances
end

def query
instances = self.class.instances
instances.each do |instance|
return instance if instance.name == @resource[:name]
end

nil
{ ensure: @property_hash[:ensure], name: @resource[:name] } unless @property_hash.empty?
end

def install
self.class.modify_snap('install', @resource[:name], @resource[:install_options])
current_ensure = query&.dig(:ensure)

# Refresh the snap if we changed the channel
if current_ensure != @resource[:ensure] && !%i[absent purged].include?(current_ensure)
modify_snap('refresh') # Refresh will switch the channel AND trigger a refresh immediately. TODO Implement switch?
else
modify_snap('install')
end
end

def update
self.class.modify_snap('refresh', @resource[:name], @resource[:install_options])
install
end

def latest
params = URI.encode_www_form(name: @resource[:name])
res = PuppetX::Snap::API.get("/v2/find?#{params}")

raise Puppet::Error, "Couldn't find latest version" if res['status-code'] != 200

# Search latest version for the specified channel. If channel is unspecified, fallback to latest/stable
channel = if @resource[:install_options].nil?
'latest/stable'
else
self.class.parse_channel(@resource[:install_options])
end

selected_channel = res['result'].first&.dig('channels', channel)
raise Puppet::Error, "No version in channel #{channel}" unless selected_channel

# Return version
selected_channel['version']
raise Puppet::Error, "Don't use ensure => latest, instead define which channel to use"
end

def uninstall
self.class.modify_snap('remove', @resource[:name])
modify_snap('remove', nil)
end

# Purge differs from remove as it doesn't save snapshot with snap's data.
# Purge differs from remove as it doesn't save a snapshot with snap's data.
def purge
self.class.modify_snap('remove', @resource[:name], ['purge'])
modify_snap('remove', ['purge'])
end

def self.installed_snaps
res = PuppetX::Snap::API.get('/v2/snaps')

raise Puppet::Error, "Could not find installed snaps (code: #{res['status-code']})" unless [200, 404].include?(res['status-code'])
def modify_snap(action, options = @resource[:install_options])
body = self.class.generate_request(action, determine_channel, options)
response = PuppetX::Snap::API.post("/v2/snaps/#{@resource[:name]}", body)
change_id = PuppetX::Snap::API.get_id_from_async_req(response)
PuppetX::Snap::API.complete(change_id)
end

res['result'].map { |hash| hash.slice('name', 'version') } if res['status-code'] == 200
def determine_channel
channel = self.class.channel_from_ensure(@resource[:ensure])
channel ||= self.class.channel_from_options(@resource[:install_options])
channel ||= 'latest/stable'
channel
end

def self.generate_request(action, options)
def self.generate_request(action, channel, options)
request = { 'action' => action }
request['channel'] = channel unless channel.nil?

if options
channel = parse_channel(options)
request['channel'] = channel unless channel.nil?

# classic, devmode and jailmode params are only available for install, refresh, revert actions.
if %w[install refresh revert].include?(action)
# classic, devmode and jailmode params are only
# available for install, refresh, revert actions.
case action
when 'install', 'refresh', 'revert'
request['classic'] = true if options.include?('classic')
request['devmode'] = true if options.include?('devmode')
request['jailmode'] = true if options.include?('jailmode')
when 'remove'
request['purge'] = true if options.include?('purge')
end

request['purge'] = true if action == 'remove' && options.include?('purge')
end

request
end

def self.modify_snap(action, name, options = nil)
req = generate_request(action, options)
response = PuppetX::Snap::API.post("/v2/snaps/#{name}", req)
change_id = PuppetX::Snap::API.get_id_from_async_req(response)
PuppetX::Snap::API.complete(change_id)
def self.channel_from_ensure(value)
value = value.to_s
case value
when 'present', 'absent', 'purged', 'installed', 'latest'
nil
else
value
end
end

def self.parse_channel(options)
if (channel = options.find { |e| %r{channel} =~ e })
return channel.split('=')[1]
def self.channel_from_options(options)
options&.find { |e| %r{channel} =~ e }&.split('=')&.last&.tap do |ch|
Puppet.warning("Install option 'channel' is deprecated, use ensure => '#{ch}' instead.")
end
end

def self.installed_snaps
res = PuppetX::Snap::API.get('/v2/snaps')
raise Puppet::Error, "Could not find installed snaps (code: #{res['status-code']})" unless [200, 404].include?(res['status-code'])

nil
res['status-code'] == 200 ? res['result'].map { |hash| hash.slice('name', 'tracking-channel') } : []
end
end
78 changes: 78 additions & 0 deletions spec/acceptance/01_snapd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
end

it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) { is_expected.to match(%r{hello-world}) }
end
end

describe 'uninstalls package' do
Expand All @@ -47,6 +51,80 @@
end

it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) { is_expected.not_to match(%r{hello-world}) }
end
end

describe 'installs package with specified version' do
let(:manifest) do
<<-PUPPET
package { 'hello-world':
ensure => 'latest/candidate',
provider => snap,
}
PUPPET
end

it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) do
is_expected.to match(%r{hello-world})
is_expected.to match(%r{candidate})
end
end
end

describe 'changes installed channel' do
let(:manifest) do
<<-PUPPET
package { 'hello-world':
ensure => 'latest/beta',
provider => snap,
}
PUPPET
end

it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) do
is_expected.to match(%r{hello-world})
is_expected.to match(%r{beta})
end
end
end
end

describe 'purges the package' do
let(:manifest) do
<<-PUPPET
package { 'hello-world':
ensure => purged,
provider => snap,
}
PUPPET
end

it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) { is_expected.not_to match(%r{hello-world}) }
end
end

# rubocop:disable RSpec/EmptyExampleGroup
describe 'Raises error when ensure => latest' do
manifest = <<-PUPPET
package { 'hello-world':
ensure => latest,
provider => snap,
}
PUPPET

apply_manifest(manifest, expect_failures: true)
end
# rubocop:enable RSpec/EmptyExampleGroup
end
Loading
Loading