From 0ce158cb6af182296fad31c72065621d242a1cf3 Mon Sep 17 00:00:00 2001 From: Spyros Pagkalos Date: Sun, 27 Mar 2022 19:19:19 +0200 Subject: [PATCH] Generic improvements --- README.md | 5 +- lib/puppet/provider/package/snap.rb | 117 ++++++++-------- spec/acceptance/01_snapd_spec.rb | 48 +++++++ spec/fixtures/responses/find_res.json | 128 ------------------ .../unit/puppet/provider/package/snap_spec.rb | 36 +++-- 5 files changed, 128 insertions(+), 206 deletions(-) delete mode 100644 spec/fixtures/responses/find_res.json diff --git a/README.md b/README.md index e922a07..55e4e13 100644 --- a/README.md +++ b/README.md @@ -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', } ``` diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 5bf1fa0..635d536 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -7,109 +7,106 @@ 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 confine feature: %i[net_http_unix_lib snapd_socket] def self.instances - instances = [] - snaps = installed_snaps - - snaps.each do |snap| - instances << new(name: snap['name'], ensure: snap['version'], provider: 'snap') + @installed_snaps ||= installed_snaps + @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] + installed = self.class.instances.find { |it| it.name == @resource['name'] } + if installed + { ensure: installed[:ensure], name: @resource[:name] } + else + { ensure: :absent, name: @resource[:name] } end + end - nil + def latest + query&.get(:ensure) end def install - self.class.modify_snap('install', @resource[:name], @resource[:install_options]) + modify_snap('install') end def update - self.class.modify_snap('refresh', @resource[:name], @resource[:install_options]) - 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'] + modify_snap('switch') 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 diff --git a/spec/acceptance/01_snapd_spec.rb b/spec/acceptance/01_snapd_spec.rb index 36b7f31..fdf4935 100644 --- a/spec/acceptance/01_snapd_spec.rb +++ b/spec/acceptance/01_snapd_spec.rb @@ -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 @@ -47,6 +51,50 @@ 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 end diff --git a/spec/fixtures/responses/find_res.json b/spec/fixtures/responses/find_res.json deleted file mode 100644 index 43238af..0000000 --- a/spec/fixtures/responses/find_res.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "type": "sync", - "status-code": 200, - "status": "OK", - "result": [ - { - "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", - "title": "Hello World", - "summary": "The 'hello-world' of snaps", - "description": "This is a simple hello world example.", - "download-size": 20480, - "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", - "name": "hello-world", - "publisher": { - "id": "canonical", - "username": "canonical", - "display-name": "Canonical", - "validation": "verified" - }, - "store-url": "https://snapcraft.io/hello-world", - "developer": "canonical", - "status": "available", - "type": "app", - "version": "6.4", - "channel": "stable", - "ignore-validation": false, - "revision": "29", - "confinement": "strict", - "private": false, - "devmode": false, - "jailmode": false, - "contact": "mailto:snaps@canonical.com", - "license": "MIT", - "media": [ - { - "type": "icon", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", - "width": 256, - "height": 256 - }, - { - "type": "screenshot", - "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png", - "width": 199, - "height": 118 - }, - { - "type": "video", - "url": "https://vimeo.com/194577403" - } - ], - "channels": { - "latest/beta": { - "revision": "29", - "confinement": "strict", - "version": "6.0", - "channel": "latest/beta", - "epoch": { - "read": [ - 0 - ], - "write": [ - 0 - ] - }, - "size": 20480, - "released-at": "2019-04-17T16:48:09.90685Z" - }, - "latest/candidate": { - "revision": "29", - "confinement": "strict", - "version": "6.4", - "channel": "latest/candidate", - "epoch": { - "read": [ - 0 - ], - "write": [ - 0 - ] - }, - "size": 20480, - "released-at": "2019-04-17T16:47:59.117114Z" - }, - "latest/edge": { - "revision": "29", - "confinement": "strict", - "version": "6.4", - "channel": "latest/edge", - "epoch": { - "read": [ - 0 - ], - "write": [ - 0 - ] - }, - "size": 20480, - "released-at": "2019-04-17T16:44:33.84163Z" - }, - "latest/stable": { - "revision": "29", - "confinement": "strict", - "version": "6.4", - "channel": "latest/stable", - "epoch": { - "read": [ - 0 - ], - "write": [ - 0 - ] - }, - "size": 20480, - "released-at": "2019-04-17T16:47:59.117114Z" - } - }, - "tracks": [ - "latest" - ], - "install-date": "2021-09-14T13:32:22.871938558+03:00" - } - ], - "sources": [ - "store" - ], - "suggested-currency": "USD" -} diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 693f3eb..86effc9 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -17,10 +17,13 @@ resource.provider end - find_res = JSON.parse(File.read('spec/fixtures/responses/find_res.json')) + before do + allow(PuppetX::Snap::API).to receive(:get).with('/v2/snaps').and_return('[]') + end context 'should have provider features' do it { is_expected.to be_installable } + it { is_expected.to be_versionable } it { is_expected.to be_install_options } it { is_expected.to be_uninstallable } it { is_expected.to be_purgeable } @@ -46,44 +49,47 @@ context 'installing without any option' do it 'generates correct request' do - response = provider.class.generate_request('install', nil) + response = provider.class.generate_request('install', nil, nil) expect(response).to eq('action' => 'install') end end - context 'installing with channel option' do + context 'installing with channel' do it 'generates correct request' do - response = provider.class.generate_request('install', ['channel=beta']) + response = provider.class.generate_request('install', 'beta', nil) expect(response).to eq('action' => 'install', 'channel' => 'beta') end end context 'installing with classic option' do it 'generates correct request' do - response = provider.class.generate_request('install', ['classic']) + response = provider.class.generate_request('install', nil, ['classic']) expect(response).to eq('action' => 'install', 'classic' => true) end end - context 'querying for latest version' do - before do - allow(PuppetX::Snap::API).to receive(:get).with('/v2/find?name=hello-world').and_return(find_res) + context 'decides the correct channel usage' do + it 'with no channel specified returns correct ensure value' do + expect(provider.determine_channel).to eq('latest/stable') end - it 'with no channel specified returns correct version from latest/stable channel' do - expect(provider.latest).to eq('6.4') + it 'with channel specified in ensure returns correct ensure value' do + resource[:ensure] = 'latest/beta' + + expect(provider.determine_channel).to eq('latest/beta') end - it 'with channel specified returns correct version from specified channel' do + it 'with channel specified in install options returns correct ensure value' do resource[:install_options] = ['channel=latest/beta'] - expect(provider.latest).to eq('6.0') + expect(provider.determine_channel).to eq('latest/beta') end - it 'with non-existent channel' do - resource[:install_options] = ['channel=latest/kokolala'] + it 'with channel specified in both ensure install options returns correct ensure value' do + resource[:install_options] = ['channel=latest/beta'] + resource[:ensure] = 'latest/candidate' # this should be preferred - expect { provider.latest }.to raise_error(%r{No version in channel latest/kokolala$}) + expect(provider.determine_channel).to eq('latest/candidate') end end end