From 6c40d719165e7798aa5793313a31be23fe2c69b9 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sun, 28 Nov 2021 21:41:14 +0200 Subject: [PATCH 01/18] Add snap_conf provider Signed-off-by: Christos Papageorgiou --- REFERENCE.md | 70 +++++++++++++- lib/puppet/provider/package/snap.rb | 92 ++---------------- lib/puppet/provider/snap_conf/snap_conf.rb | 87 +++++++++++++++++ lib/puppet/type/snap_conf.rb | 37 ++++++++ lib/puppet_x/snap/api.rb | 93 +++++++++++++++++++ .../{snapd_spec.rb => 01_snapd_spec.rb} | 0 spec/acceptance/02_snap_conf_spec.rb | 42 +++++++++ .../unit/puppet/provider/package/snap_spec.rb | 37 +------- .../provider/snap_conf/snap_conf_spec.rb | 23 +++++ spec/unit/puppet_x/snap/api_spec.rb | 41 ++++++++ 10 files changed, 402 insertions(+), 120 deletions(-) create mode 100644 lib/puppet/provider/snap_conf/snap_conf.rb create mode 100644 lib/puppet/type/snap_conf.rb create mode 100644 lib/puppet_x/snap/api.rb rename spec/acceptance/{snapd_spec.rb => 01_snapd_spec.rb} (100%) create mode 100644 spec/acceptance/02_snap_conf_spec.rb create mode 100644 spec/unit/puppet/provider/snap_conf/snap_conf_spec.rb create mode 100644 spec/unit/puppet_x/snap/api_spec.rb diff --git a/REFERENCE.md b/REFERENCE.md index 30ab256..16f4529 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -8,6 +8,10 @@ * [`snap`](#snap) +### Resource types + +* [`snap_conf`](#snap_conf): Manage snap configuration both system wide and snap specific. + ## Classes ### `snap` @@ -23,6 +27,7 @@ The following parameters are available in the `snap` class: * [`service_enable`](#service_enable) * [`core_snap_ensure`](#core_snap_ensure) * [`manage_repo`](#manage_repo) +* [`net_http_unix_ensure`](#net_http_unix_ensure) ##### `package_ensure` @@ -62,5 +67,68 @@ Data type: `Boolean` Whether we should manage EPEL repo or not. -Default value: ``true`` +Default value: ``false`` + +##### `net_http_unix_ensure` + +Data type: `Enum['present', 'installed', 'absent']` + +The state of net_http_unix gem. + +Default value: `'installed'` + +## Resource types + +### `snap_conf` + +Manage snap configuration both system wide and snap specific. + +#### Properties + +The following properties are available in the `snap_conf` type. + +##### `ensure` + +Valid values: `present`, `absent` + +The desired state of the snap configuration. + +Default value: `present` + +#### Parameters + +The following parameters are available in the `snap_conf` type. + +* [`conf`](#conf) +* [`name`](#name) +* [`provider`](#provider) +* [`snap`](#snap) +* [`value`](#value) + +##### `conf` + +Name of configuration option. + +Default value: `''` + +##### `name` + +namevar + +An unique name for this define. + +##### `provider` + +The specific backend to use for this `snap_conf` resource. You will seldom need to specify this --- Puppet will usually +discover the appropriate provider for your platform. + +##### `snap` + +The snap to configure the value for. This can be the reserved name system for system wide configurations. + +Default value: `''` + +##### `value` + +Value of configuration option. diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index cf95083..5c41328 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true require 'puppet/provider/package' -require 'net/http' -require 'socket' -require 'json' +require 'puppet_x/snap/api' Puppet::Type.type(:package).provide :snap, parent: Puppet::Provider::Package do desc "Package management via Snap. @@ -43,7 +41,8 @@ def update end def latest - res = self.class.call_api('GET', "/v2/find?name=#{@resource[:name]}") + params = URI.encode_www_form(name: @resource[:name]) + res = PuppetX::Snap::API.call_api('GET', "/v2/find?#{params}") raise Puppet::Error, "Couldn't find latest version" if res['status-code'] != 200 @@ -69,91 +68,14 @@ def purge self.class.modify_snap('remove', @resource[:name], ['purge']) end - def self.call_api(method, url, data = nil) - socket = Net::BufferedIO.new(UNIXSocket.new('/run/snapd.socket')) - - request = if method == 'POST' - req = Net::HTTP::Post.new(url) - req.body = data.to_json - req - else - Net::HTTP::Get.new(url) - end - - request['Host'] = 'localhost' - request['Accept'] = 'application/json' - request['Content-Type'] = 'application/json' - request.exec(socket, '1.1', url) - - response = nil - retried = 0 - max_retries = 5 - # Read timeout can happen while installing core snap. The snap daemon briefly restarts - # which drops the connection to the socket. - loop do - response = Net::HTTPResponse.read_new(socket) - break unless response.is_a?(Net::HTTPContinue) - rescue Net::ReadTimeout, Net::OpenTimeout - raise Puppet::Error, "Got timeout wile calling the api #{retried} times! Giving up..." if retried > max_retries - - Puppet.debug('Got timeout while calling the api, retrying...') - retried += 1 - retry - end - # rubocop:disable Lint/EmptyBlock - response.reading_body(socket, request.response_body_permitted?) {} - # rubocop:enable Lint/EmptyBlock - - JSON.parse(response.body) - end - def self.installed_snaps - res = call_api('GET', '/v2/snaps') + res = PuppetX::Snap::API.call_api('GET', '/v2/snaps') raise Puppet::Error, "Could not find installed snaps (code: #{res['status-code']})" unless [200, 404].include?(res['status-code']) res['result'].map { |hash| hash.slice('name', 'version') } if res['status-code'] == 200 end - # Helper method to return the change ID from a asynchronous request response. - def self.get_id_from_async_req(request) - # If the request failed raise an error - raise Puppet::Error, "Request failed with #{request['result']['message']}" if request['type'] == 'error' - - request['change'] - end - - # Get the status of a change - # - # @param id The change ID to search for. - def self.get_status(id) - call_api('GET', "/v2/changes/#{id}") - end - - # Queries the API for a specific change and waits until it has - # been completed. - # - # @param id The change ID to search for. - def self.complete(id) - completed = false - until completed - res = get_status(id) - case res['result']['status'] - when 'Do', 'Doing', 'Undoing', 'Undo' - # Still running - # Wait a little bit before hitting the API again! - sleep(1) - next - when 'Abort', 'Hold', 'Error' - raise Puppet::Error, "Error while executing the request #{res}" - when 'Done' - completed = true - else - raise Puppet::Error, "Unknown status #{res}" - end - end - end - def self.generate_request(action, options) request = { 'action' => action } @@ -176,9 +98,9 @@ def self.generate_request(action, options) def self.modify_snap(action, name, options = nil) req = generate_request(action, options) - response = call_api('POST', "/v2/snaps/#{name}", req) - change_id = get_id_from_async_req(response) - complete(change_id) + response = PuppetX::Snap::API.call_api('POST', "/v2/snaps/#{name}", req) + change_id = PuppetX::Snap::API.get_id_from_async_req(response) + PuppetX::Snap::API.complete(change_id) end def self.parse_channel(options) diff --git a/lib/puppet/provider/snap_conf/snap_conf.rb b/lib/puppet/provider/snap_conf/snap_conf.rb new file mode 100644 index 0000000..8868a93 --- /dev/null +++ b/lib/puppet/provider/snap_conf/snap_conf.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'puppet_x/snap/api' + +Puppet::Type.type(:snap_conf).provide(:snap_conf) do + desc 'Manage snap configuration both system wide and snap specific.' + + def create + save_conf + end + + def destroy + save_conf + end + + def exists? + params = URI.encode_www_form(keys: @resource[:conf]) + res = PuppetX::Snap::API.call_api('GET', "/v2/snaps/#{@resource[:snap]}/conf?#{params}") + + case res['status-code'] + when 200 + # If we reached here the resource exists. If ensure == absent then return true in order to remove it + true if res['result'][@resource[:conf]] == @resource[:value] || @resource[:ensure] == :absent + when 400 + return false if res['result']['kind'] == 'option-not-found' + + raise Puppet::Error, "Error while executing the request #{res}" + else + raise Puppet::Error, "Error while executing the request #{res}" + end + end + + def save_conf + value = if @resource[:ensure] == :absent + nil + else + @resource[:value] + end + + data = { + @resource[:conf] => value + } + + res = PuppetX::Snap::API.call_api('PUT', "/v2/snaps/#{@resource[:snap]}/conf", data) + change_id = PuppetX::Snap::API.get_id_from_async_req(res) + PuppetX::Snap::API.complete(change_id) + end + + def snap + @resource[:snap] + end + + def snap=(value) + @resource[:snap] = value + save_conf + end + + def conf + @resource[:conf] + end + + def conf=(value) + @resource[:conf] = value + save_conf + end + + def value + params = URI.encode_www_form(keys: @resource[:conf]) + res = PuppetX::Snap::API.call_api('GET', "/v2/snaps/#{@resource[:snap]}/conf?#{params}") + + case res['status-code'] + when 200 + res['result'][@resource[:conf]] + when 400 + return nil if res['result']['kind'] == 'option-not-found' + + raise Puppet::Error, "Error while executing the request #{res}" + else + raise Puppet::Error, "Error while executing the request #{res}" + end + end + + def value=(value) + @resource[:value] = value + save_conf + end +end diff --git a/lib/puppet/type/snap_conf.rb b/lib/puppet/type/snap_conf.rb new file mode 100644 index 0000000..305a4f0 --- /dev/null +++ b/lib/puppet/type/snap_conf.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +Puppet::Type.newtype(:snap_conf) do + @doc = 'Manage snap configuration both system wide and snap specific.' + + ensurable do + defaultvalues + defaultto :present + desc 'The desired state of the snap configuration.' + end + + newparam(:name, namevar: true) do + desc 'An unique name for this define.' + end + + newparam(:snap) do + desc 'The snap to configure the value for. This can be the reserved name system for system wide configurations.' + defaultto '' + + validate do |value| + raise ArgumentError, 'snap parameter must not be empty!' if value == '' + end + end + + newparam(:conf) do + desc 'Name of configuration option.' + defaultto '' + + validate do |value| + raise ArgumentError, 'conf parameter must not be empty!' if value == '' + end + end + + newparam(:value) do + desc 'Value of configuration option.' + end +end diff --git a/lib/puppet_x/snap/api.rb b/lib/puppet_x/snap/api.rb new file mode 100644 index 0000000..7fe38c4 --- /dev/null +++ b/lib/puppet_x/snap/api.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'net/http' +require 'socket' +require 'json' + +module PuppetX + module Snap + module API + def self.call_api(method, url, data = nil) + socket = Net::BufferedIO.new(UNIXSocket.new('/run/snapd.socket')) + + request = case method + when 'POST' + req = Net::HTTP::Post.new(url) + req.body = data.to_json + req + when 'PUT' + req = Net::HTTP::Put.new(url) + req.body = data.to_json + req + else + Net::HTTP::Get.new(url) + end + + request['Host'] = 'localhost' + request['Accept'] = 'application/json' + request['Content-Type'] = 'application/json' + request.exec(socket, '1.1', url) + + response = nil + retried = 0 + max_retries = 5 + # Read timeout can happen while installing core snap. The snap daemon briefly restarts + # which drops the connection to the socket. + loop do + response = Net::HTTPResponse.read_new(socket) + break unless response.is_a?(Net::HTTPContinue) + rescue Net::ReadTimeout, Net::OpenTimeout + raise Puppet::Error, "Got timeout wile calling the api #{retried} times! Giving up..." if retried > max_retries + + Puppet.debug('Got timeout while calling the api, retrying...') + retried += 1 + retry + end + # rubocop:disable Lint/EmptyBlock + response.reading_body(socket, request.response_body_permitted?) {} + # rubocop:enable Lint/EmptyBlock + + JSON.parse(response.body) + end + + # Helper method to return the change ID from a asynchronous request response. + def self.get_id_from_async_req(request) + # If the request failed raise an error + raise Puppet::Error, "Request failed with #{request['result']['message']}" if request['type'] == 'error' + + request['change'] + end + + # Get the status of a change + # + # @param id The change ID to search for. + def self.get_status(id) + call_api('GET', "/v2/changes/#{id}") + end + + # Queries the API for a specific change and waits until it has + # been completed. + # + # @param id The change ID to search for. + def self.complete(id) + completed = false + until completed + res = get_status(id) + case res['result']['status'] + when 'Do', 'Doing', 'Undoing', 'Undo' + # Still running + # Wait a little bit before hitting the API again! + sleep(1) + next + when 'Abort', 'Hold', 'Error' + raise Puppet::Error, "Error while executing the request #{res}" + when 'Done' + completed = true + else + raise Puppet::Error, "Unknown status #{res}" + end + end + end + end + end +end diff --git a/spec/acceptance/snapd_spec.rb b/spec/acceptance/01_snapd_spec.rb similarity index 100% rename from spec/acceptance/snapd_spec.rb rename to spec/acceptance/01_snapd_spec.rb diff --git a/spec/acceptance/02_snap_conf_spec.rb b/spec/acceptance/02_snap_conf_spec.rb new file mode 100644 index 0000000..6d20ae0 --- /dev/null +++ b/spec/acceptance/02_snap_conf_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper_acceptance' + +describe 'snap_conf resource' do + context 'snap_conf' do + let(:manifest) do + <<-EOS + snap_conf { 'test1': + ensure => present, + snap => 'system', + conf => 'refresh.retain', + value => '3' + } + EOS + end + + it_behaves_like 'an idempotent resource' + + describe command('snap get system refresh.retain') do + its(:stdout) { is_expected.to match %r{3} } + end + end + + context 'destroy resource' do + let(:manifest) do + <<-EOS + snap_conf { 'test1': + ensure => absent, + snap => 'system', + conf => 'refresh.retain', + } + EOS + end + + it_behaves_like 'an idempotent resource' + + describe command('snap get system refresh.retain') do + its(:stderr) { is_expected.to match %r{error: snap "core" has no "refresh.retain" configuration option} } + end + end +end diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 1e444ae..658fb6b 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require 'puppet_x/snap/api' describe Puppet::Type.type(:package).provider(:snap) do let(:name) { 'hello-world' } @@ -16,11 +17,6 @@ resource.provider end - async_change_id_res = JSON.parse(File.read('spec/fixtures/responses/async_change_id_res.json')) - error_res = JSON.parse(File.read('spec/fixtures/responses/error_res.json')) - change_status_doing = JSON.parse(File.read('spec/fixtures/responses/change_status_doing.json')) - change_status_done = JSON.parse(File.read('spec/fixtures/responses/change_status_done.json')) - change_status_error = JSON.parse(File.read('spec/fixtures/responses/change_status_error.json')) find_res = JSON.parse(File.read('spec/fixtures/responses/find_res.json')) context 'should have provider features' do @@ -69,43 +65,16 @@ end end - context 'calling async operations' do - it 'raises an error if response is an error' do - expect { provider.class.get_id_from_async_req(error_res) }.to raise_error(Puppet::Error) - end - - it 'gets correct change id from response' do - id = provider.class.get_id_from_async_req(async_change_id_res) - expect(id).to eq('77') - end - end - - context 'completing async operations' do - it 'raises an error if response is an error' do - allow(described_class).to receive(:get_status).with('10').and_return(change_status_error) - - expect { provider.class.complete('10') }.to raise_error(Puppet::Error) - end - - it 'sleeps for 1 second if response hasn\'t completed' do - allow(described_class).to receive(:get_status).with('10').and_return(change_status_doing, change_status_done) - allow(described_class).to receive(:sleep) - provider.class.complete('10') - - expect(described_class).to have_received(:sleep).with(1) - end - end - context 'querying for latest version' do it 'with no channel specified returns correct version from stable channel' do - allow(described_class).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) + allow(PuppetX::Snap::API).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) expect(provider.latest).to eq('6.4') end it 'with channel specified returns correct version from specified channel' do resource[:install_options] = ['channel=beta'] - allow(described_class).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) + allow(PuppetX::Snap::API).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) expect(provider.latest).to eq('6.0') end diff --git a/spec/unit/puppet/provider/snap_conf/snap_conf_spec.rb b/spec/unit/puppet/provider/snap_conf/snap_conf_spec.rb new file mode 100644 index 0000000..e059cbe --- /dev/null +++ b/spec/unit/puppet/provider/snap_conf/snap_conf_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet_x/snap/api' + +describe Puppet::Type.type(:snap_conf) do + let(:name) { 'hello-world' } + let(:resource) do + Puppet::Type.type(:snap_conf).new( + name: name, + snap: 'system', + conf: 'refresh.retain', + value: '3' + ) + end + let(:provider) do + resource.provider + end + + it 'defaults to ensure => present' do + expect(resource[:ensure]).to eq :present + end +end diff --git a/spec/unit/puppet_x/snap/api_spec.rb b/spec/unit/puppet_x/snap/api_spec.rb new file mode 100644 index 0000000..ad89b27 --- /dev/null +++ b/spec/unit/puppet_x/snap/api_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet_x/snap/api' + +module PuppetX::Snap + describe API do + async_change_id_res = JSON.parse(File.read('spec/fixtures/responses/async_change_id_res.json')) + error_res = JSON.parse(File.read('spec/fixtures/responses/error_res.json')) + change_status_doing = JSON.parse(File.read('spec/fixtures/responses/change_status_doing.json')) + change_status_done = JSON.parse(File.read('spec/fixtures/responses/change_status_done.json')) + change_status_error = JSON.parse(File.read('spec/fixtures/responses/change_status_error.json')) + + context 'calling async operations' do + it 'raises an error if response is an error' do + expect { described_class.get_id_from_async_req(error_res) }.to raise_error(Puppet::Error) + end + + it 'gets correct change id from response' do + id = described_class.get_id_from_async_req(async_change_id_res) + expect(id).to eq('77') + end + end + + context 'completing async operations' do + it 'raises an error if response is an error' do + allow(described_class).to receive(:get_status).with('10').and_return(change_status_error) + + expect { described_class.complete('10') }.to raise_error(Puppet::Error) + end + + it 'sleeps for 1 second if response hasn\'t completed' do + allow(described_class).to receive(:get_status).with('10').and_return(change_status_doing, change_status_done) + allow(described_class).to receive(:sleep) + described_class.complete('10') + + expect(described_class).to have_received(:sleep).with(1) + end + end + end +end From 3fef6ae29789874e1150779d7e9ba26f8babef59 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Fri, 25 Mar 2022 16:53:18 +0200 Subject: [PATCH 02/18] Install net_http_unix Signed-off-by: Christos Papageorgiou --- .sync.yml | 4 ++++ Gemfile | 1 + manifests/init.pp | 20 +++++++++++++------- spec/classes/init_spec.rb | 3 ++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.sync.yml b/.sync.yml index 130aaeb..d71e85b 100644 --- a/.sync.yml +++ b/.sync.yml @@ -1,4 +1,8 @@ --- +Gemfile: + optional: + ':test': + - gem: 'net_http_unix' .puppet-lint.rc: enabled_lint_checks: - parameter_documentation diff --git a/Gemfile b/Gemfile index 2b731b9..d14ad99 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ group :test do gem 'simplecov-console', :require => false gem 'puppet_metadata', '~> 1.0', :require => false gem 'puppet-lint-param-docs', :require => false + gem 'net_http_unix', :require => false end group :development do diff --git a/manifests/init.pp b/manifests/init.pp index b7a4b93..c8b15aa 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -5,12 +5,14 @@ # @param service_enable Run the system service on boot. # @param core_snap_ensure The state of the snap `core`. # @param manage_repo Whether we should manage EPEL repo or not. +# @param net_http_unix_ensure The state of net_http_unix gem. class snap ( - String[1] $package_ensure = 'installed', - Enum['stopped', 'running'] $service_ensure = 'running', - Boolean $service_enable = true, - String[1] $core_snap_ensure = 'installed', - Boolean $manage_repo = false, + String[1] $package_ensure = 'installed', + Enum['stopped', 'running'] $service_ensure = 'running', + Boolean $service_enable = true, + String[1] $core_snap_ensure = 'installed', + Boolean $manage_repo = false, + Enum['present', 'installed', 'absent'] $net_http_unix_ensure = 'installed', ) { if $manage_repo { include epel @@ -35,9 +37,13 @@ require => Package['snapd'], } - package { 'core': + -> package { 'net_http_unix': + ensure => $net_http_unix_ensure, + provider => 'puppet_gem', + } + + -> package { 'core': ensure => $core_snap_ensure, provider => 'snap', - require => Service['snapd'], } } diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index a25e324..1b931f7 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -15,7 +15,8 @@ it { is_expected.to contain_package('snapd').with_ensure('installed') } it { is_expected.to contain_service('snapd').with_ensure('running').with_enable(true).that_requires('Package[snapd]') } - it { is_expected.to contain_package('core').with_ensure('installed').with_provider('snap').that_requires('Service[snapd]') } + it { is_expected.to contain_package('net_http_unix').with_ensure('installed').with_provider('puppet_gem').that_requires('Service[snapd]') } + it { is_expected.to contain_package('core').with_ensure('installed').with_provider('snap').that_requires(%w[Service[snapd] Package[net_http_unix]]) } end end end From 35f1a696e8ccdebf94367d3e28e78e870a54153d Mon Sep 17 00:00:00 2001 From: Christos Gkoumas Date: Tue, 15 Mar 2022 23:00:53 +0200 Subject: [PATCH 03/18] Add PuppetX::Snap::API#{post, put, get} dynamically --- lib/puppet/provider/package/snap.rb | 6 +- lib/puppet/provider/snap_conf/snap_conf.rb | 6 +- lib/puppet_x/snap/api.rb | 135 +++++++++--------- .../unit/puppet/provider/package/snap_spec.rb | 4 +- 4 files changed, 73 insertions(+), 78 deletions(-) diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 5c41328..22f91dc 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -42,7 +42,7 @@ def update def latest params = URI.encode_www_form(name: @resource[:name]) - res = PuppetX::Snap::API.call_api('GET', "/v2/find?#{params}") + res = PuppetX::Snap::API.get("/v2/find?#{params}") raise Puppet::Error, "Couldn't find latest version" if res['status-code'] != 200 @@ -69,7 +69,7 @@ def purge end def self.installed_snaps - res = PuppetX::Snap::API.call_api('GET', '/v2/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']) @@ -98,7 +98,7 @@ def self.generate_request(action, options) def self.modify_snap(action, name, options = nil) req = generate_request(action, options) - response = PuppetX::Snap::API.call_api('POST', "/v2/snaps/#{name}", req) + 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) end diff --git a/lib/puppet/provider/snap_conf/snap_conf.rb b/lib/puppet/provider/snap_conf/snap_conf.rb index 8868a93..85cc952 100644 --- a/lib/puppet/provider/snap_conf/snap_conf.rb +++ b/lib/puppet/provider/snap_conf/snap_conf.rb @@ -15,7 +15,7 @@ def destroy def exists? params = URI.encode_www_form(keys: @resource[:conf]) - res = PuppetX::Snap::API.call_api('GET', "/v2/snaps/#{@resource[:snap]}/conf?#{params}") + res = PuppetX::Snap::API.get("/v2/snaps/#{@resource[:snap]}/conf?#{params}") case res['status-code'] when 200 @@ -41,7 +41,7 @@ def save_conf @resource[:conf] => value } - res = PuppetX::Snap::API.call_api('PUT', "/v2/snaps/#{@resource[:snap]}/conf", data) + res = PuppetX::Snap::API.put("/v2/snaps/#{@resource[:snap]}/conf", data) change_id = PuppetX::Snap::API.get_id_from_async_req(res) PuppetX::Snap::API.complete(change_id) end @@ -66,7 +66,7 @@ def conf=(value) def value params = URI.encode_www_form(keys: @resource[:conf]) - res = PuppetX::Snap::API.call_api('GET', "/v2/snaps/#{@resource[:snap]}/conf?#{params}") + res = PuppetX::Snap::API.get("/v2/snaps/#{@resource[:snap]}/conf?#{params}") case res['status-code'] when 200 diff --git a/lib/puppet_x/snap/api.rb b/lib/puppet_x/snap/api.rb index 7fe38c4..ca32816 100644 --- a/lib/puppet_x/snap/api.rb +++ b/lib/puppet_x/snap/api.rb @@ -1,90 +1,85 @@ # frozen_string_literal: true -require 'net/http' +require 'net_http_unix' if Puppet.features.net_http_unix_lib? require 'socket' require 'json' module PuppetX module Snap module API - def self.call_api(method, url, data = nil) - socket = Net::BufferedIO.new(UNIXSocket.new('/run/snapd.socket')) + class << self + %w[post put get].each do |method| + define_method(method) do |url, data = nil| + request = Object.const_get("Net::HTTP::#{method.capitalize}").new(url) - request = case method - when 'POST' - req = Net::HTTP::Post.new(url) - req.body = data.to_json - req - when 'PUT' - req = Net::HTTP::Put.new(url) - req.body = data.to_json - req - else - Net::HTTP::Get.new(url) - end + request.body = data.to_json if data - request['Host'] = 'localhost' - request['Accept'] = 'application/json' - request['Content-Type'] = 'application/json' - request.exec(socket, '1.1', url) + request['Host'] = 'localhost' + request['Accept'] = 'application/json' + request['Content-Type'] = 'application/json' - response = nil - retried = 0 - max_retries = 5 - # Read timeout can happen while installing core snap. The snap daemon briefly restarts - # which drops the connection to the socket. - loop do - response = Net::HTTPResponse.read_new(socket) - break unless response.is_a?(Net::HTTPContinue) - rescue Net::ReadTimeout, Net::OpenTimeout - raise Puppet::Error, "Got timeout wile calling the api #{retried} times! Giving up..." if retried > max_retries - - Puppet.debug('Got timeout while calling the api, retrying...') - retried += 1 - retry + call_api(request) + end end - # rubocop:disable Lint/EmptyBlock - response.reading_body(socket, request.response_body_permitted?) {} - # rubocop:enable Lint/EmptyBlock - JSON.parse(response.body) - end + def call_api(request) + client = ::NetX::HTTPUnix.new('unix:///run/snapd.socket') - # Helper method to return the change ID from a asynchronous request response. - def self.get_id_from_async_req(request) - # If the request failed raise an error - raise Puppet::Error, "Request failed with #{request['result']['message']}" if request['type'] == 'error' + response = nil + retried = 0 + max_retries = 5 + # Read timeout can happen while installing core snap. The snap daemon briefly restarts + # which drops the connection to the socket. + loop do + response = client.request(request) + break unless response.is_a?(Net::HTTPContinue) + rescue Net::ReadTimeout, Net::OpenTimeout + raise Puppet::Error, "Got timeout wile calling the api #{retried} times! Giving up..." if retried > max_retries - request['change'] - end + Puppet.debug('Got timeout while calling the api, retrying...') + retried += 1 + retry + end - # Get the status of a change - # - # @param id The change ID to search for. - def self.get_status(id) - call_api('GET', "/v2/changes/#{id}") - end + JSON.parse(response.body) + end + + # Helper method to return the change ID from a asynchronous request response. + def get_id_from_async_req(request) + # If the request failed raise an error + raise Puppet::Error, "Request failed with #{request['result']['message']}" if request['type'] == 'error' + + request['change'] + end + + # Get the status of a change + # + # @param id The change ID to search for. + def get_status(id) + get("/v2/changes/#{id}") + end - # Queries the API for a specific change and waits until it has - # been completed. - # - # @param id The change ID to search for. - def self.complete(id) - completed = false - until completed - res = get_status(id) - case res['result']['status'] - when 'Do', 'Doing', 'Undoing', 'Undo' - # Still running - # Wait a little bit before hitting the API again! - sleep(1) - next - when 'Abort', 'Hold', 'Error' - raise Puppet::Error, "Error while executing the request #{res}" - when 'Done' - completed = true - else - raise Puppet::Error, "Unknown status #{res}" + # Queries the API for a specific change and waits until it has + # been completed. + # + # @param id The change ID to search for. + def complete(id) + completed = false + until completed + res = get_status(id) + case res['result']['status'] + when 'Do', 'Doing', 'Undoing', 'Undo' + # Still running + # Wait a little bit before hitting the API again! + sleep(1) + next + when 'Abort', 'Hold', 'Error' + raise Puppet::Error, "Error while executing the request #{res}" + when 'Done' + completed = true + else + raise Puppet::Error, "Unknown status #{res}" + end end end end diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 658fb6b..1699519 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -67,14 +67,14 @@ context 'querying for latest version' do it 'with no channel specified returns correct version from stable channel' do - allow(PuppetX::Snap::API).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) + allow(PuppetX::Snap::API).to receive(:get).with('/v2/find?name=hello-world').and_return(find_res) expect(provider.latest).to eq('6.4') end it 'with channel specified returns correct version from specified channel' do resource[:install_options] = ['channel=beta'] - allow(PuppetX::Snap::API).to receive(:call_api).with('GET', '/v2/find?name=hello-world').and_return(find_res) + allow(PuppetX::Snap::API).to receive(:get).with('/v2/find?name=hello-world').and_return(find_res) expect(provider.latest).to eq('6.0') end From ac0500d85a5e249fc4202de9deea955bfd038582 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Fri, 25 Mar 2022 17:22:07 +0200 Subject: [PATCH 04/18] Confine providers to net_http_unix feature Signed-off-by: Christos Papageorgiou --- lib/puppet/feature/net_http_unix_lib.rb | 5 +++++ lib/puppet/provider/package/snap.rb | 1 + lib/puppet/provider/snap_conf/snap_conf.rb | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 lib/puppet/feature/net_http_unix_lib.rb diff --git a/lib/puppet/feature/net_http_unix_lib.rb b/lib/puppet/feature/net_http_unix_lib.rb new file mode 100644 index 0000000..6735fda --- /dev/null +++ b/lib/puppet/feature/net_http_unix_lib.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require 'puppet/util/feature' + +Puppet.features.add(:net_http_unix_lib, libs: 'net_http_unix') diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 22f91dc..81ce2d2 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -11,6 +11,7 @@ commands snap_cmd: '/usr/bin/snap' has_feature :installable, :install_options, :uninstallable, :purgeable + confine feature: :net_http_unix_lib def self.instances instances = [] diff --git a/lib/puppet/provider/snap_conf/snap_conf.rb b/lib/puppet/provider/snap_conf/snap_conf.rb index 85cc952..6e52cf2 100644 --- a/lib/puppet/provider/snap_conf/snap_conf.rb +++ b/lib/puppet/provider/snap_conf/snap_conf.rb @@ -5,6 +5,8 @@ Puppet::Type.type(:snap_conf).provide(:snap_conf) do desc 'Manage snap configuration both system wide and snap specific.' + confine feature: :net_http_unix_lib + def create save_conf end From 6ae313795851ddc725d974e6c192edd710856646 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 15:13:30 +0200 Subject: [PATCH 05/18] Add new snapd_socket feature and confine the providers to it --- lib/puppet/feature/snapd_socket.rb | 7 +++++++ lib/puppet/provider/package/snap.rb | 2 +- lib/puppet/provider/snap_conf/snap_conf.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 lib/puppet/feature/snapd_socket.rb diff --git a/lib/puppet/feature/snapd_socket.rb b/lib/puppet/feature/snapd_socket.rb new file mode 100644 index 0000000..87a6c0c --- /dev/null +++ b/lib/puppet/feature/snapd_socket.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'puppet/util/feature' + +Puppet.features.add(:snapd_socket) do + true if File.exist?('/run/snapd.socket') +end diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 81ce2d2..fd90154 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -11,7 +11,7 @@ commands snap_cmd: '/usr/bin/snap' has_feature :installable, :install_options, :uninstallable, :purgeable - confine feature: :net_http_unix_lib + confine feature: %i[net_http_unix_lib snapd_socket] def self.instances instances = [] diff --git a/lib/puppet/provider/snap_conf/snap_conf.rb b/lib/puppet/provider/snap_conf/snap_conf.rb index 6e52cf2..6ffb6cd 100644 --- a/lib/puppet/provider/snap_conf/snap_conf.rb +++ b/lib/puppet/provider/snap_conf/snap_conf.rb @@ -5,7 +5,7 @@ Puppet::Type.type(:snap_conf).provide(:snap_conf) do desc 'Manage snap configuration both system wide and snap specific.' - confine feature: :net_http_unix_lib + confine feature: %i[net_http_unix_lib snapd_socket] def create save_conf From 7d330a06f329e3f878c0f3627eb3befa92b33beb Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 11:45:36 +0200 Subject: [PATCH 06/18] Add back acceptance test for installing/uninstalling snap --- spec/acceptance/01_snapd_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/acceptance/01_snapd_spec.rb b/spec/acceptance/01_snapd_spec.rb index 66c0ab9..36b7f31 100644 --- a/spec/acceptance/01_snapd_spec.rb +++ b/spec/acceptance/01_snapd_spec.rb @@ -21,4 +21,32 @@ it { is_expected.to be_socket } end end + + context 'package resource' do + describe 'installs package' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => installed, + provider => snap, + } + PUPPET + end + + it_behaves_like 'an idempotent resource' + end + + describe 'uninstalls package' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => absent, + provider => snap, + } + PUPPET + end + + it_behaves_like 'an idempotent resource' + end + end end From 9f133761750d672b79ff0fe5d744b5349a6e30e5 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 11:52:08 +0200 Subject: [PATCH 07/18] Add CentOS 9 support --- metadata.json | 12 ++++++++---- spec/spec_helper_acceptance.rb | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/metadata.json b/metadata.json index ca7e317..1d26f33 100644 --- a/metadata.json +++ b/metadata.json @@ -14,28 +14,32 @@ "operatingsystem": "RedHat", "operatingsystemrelease": [ "7", - "8" + "8", + "9" ] }, { "operatingsystem": "CentOS", "operatingsystemrelease": [ "7", - "8" + "8", + "9" ] }, { "operatingsystem": "OracleLinux", "operatingsystemrelease": [ "7", - "8" + "8", + "9" ] }, { "operatingsystem": "Scientific", "operatingsystemrelease": [ "7", - "8" + "8", + "9" ] }, { diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb index 2d49afa..fa9d1b9 100644 --- a/spec/spec_helper_acceptance.rb +++ b/spec/spec_helper_acceptance.rb @@ -27,7 +27,7 @@ apply_manifest_on(host, pp, catch_failures: true) apply_manifest_on(host, debian, catch_failures: true) if fact('os.family') == 'Debian' - install_module_from_forge_on(host, 'puppet/epel', '>= 3.1.0 < 4.0.0') if fact('os.family') == 'RedHat' + install_module_from_forge_on(host, 'puppet/epel', '>= 3.1.0 < 5.0.0') if fact('os.family') == 'RedHat' end Dir['./spec/support/acceptance/**/*.rb'].sort.each { |f| require f } From a9614ad912d1758bc775216e5df55b9b931ff612 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 16:40:50 +0200 Subject: [PATCH 08/18] modulesync 5.2.0 --- .github/CONTRIBUTING.md | 7 ++- .github/workflows/ci.yml | 84 +++-------------------------------- .github/workflows/release.yml | 32 +++++-------- .msync.yml | 2 +- Dockerfile | 2 +- Gemfile | 7 ++- Rakefile | 2 +- 7 files changed, 25 insertions(+), 111 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 048d2b5..8b466cf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -124,7 +124,7 @@ If you have Ruby 2.x or want a specific version of Puppet, you must set an environment variable such as: ```sh -export PUPPET_VERSION="~> 5.5.6" +export PUPPET_GEM_VERSION="~> 6.1.0" ``` You can install all needed gems for spec tests into the modules directory by @@ -232,17 +232,16 @@ simple tests against it after applying the module. You can run this with: ```sh -BEAKER_setfile=debian10-x64 bundle exec rake beaker +BEAKER_setfile=debian11-64 bundle exec rake beaker ``` You can replace the string `debian10` with any common operating system. The following strings are known to work: -* ubuntu1604 * ubuntu1804 * ubuntu2004 -* debian9 * debian10 +* debian11 * centos7 * centos8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d08d05e..8a07791 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,84 +7,12 @@ name: CI on: pull_request concurrency: - group: ${{ github.head_ref }} + group: ${{ github.ref_name }} cancel-in-progress: true jobs: - setup_matrix: - name: 'Setup Test Matrix' - runs-on: ubuntu-latest - timeout-minutes: 40 - outputs: - puppet_unit_test_matrix: ${{ steps.get-outputs.outputs.puppet_unit_test_matrix }} - github_action_test_matrix: ${{ steps.get-outputs.outputs.github_action_test_matrix }} - env: - BUNDLE_WITHOUT: development:system_tests:release - steps: - - uses: actions/checkout@v2 - - name: Setup ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.0' - bundler-cache: true - - name: Run static validations - run: bundle exec rake validate lint check - - name: Run rake rubocop - run: bundle exec rake rubocop - - name: Setup Test Matrix - id: get-outputs - run: bundle exec metadata2gha --use-fqdn --pidfile-workaround false - - unit: - needs: setup_matrix - runs-on: ubuntu-latest - timeout-minutes: 40 - strategy: - fail-fast: false - matrix: - include: ${{fromJson(needs.setup_matrix.outputs.puppet_unit_test_matrix)}} - env: - BUNDLE_WITHOUT: development:system_tests:release - PUPPET_VERSION: "~> ${{ matrix.puppet }}.0" - name: Puppet ${{ matrix.puppet }} (Ruby ${{ matrix.ruby }}) - steps: - - uses: actions/checkout@v2 - - name: Setup ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true - - name: Run tests - run: bundle exec rake parallel_spec - - acceptance: - needs: setup_matrix - runs-on: ubuntu-latest - env: - BUNDLE_WITHOUT: development:test:release - strategy: - fail-fast: false - matrix: - include: ${{fromJson(needs.setup_matrix.outputs.github_action_test_matrix)}} - name: ${{ matrix.puppet.name }} - ${{ matrix.setfile.name }} - steps: - - uses: actions/checkout@v2 - - name: Setup ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.0' - bundler-cache: true - - name: Run tests - run: bundle exec rake beaker - env: - BEAKER_PUPPET_COLLECTION: ${{ matrix.puppet.collection }} - BEAKER_setfile: ${{ matrix.setfile.value }} - - tests: - needs: - - unit - - acceptance - runs-on: ubuntu-latest - name: Test suite - steps: - - run: echo Test suite completed + puppet: + name: Puppet + uses: voxpupuli/gha-puppet/.github/workflows/beaker.yml@v1 + with: + pidfile_workaround: 'false' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9edb616..37438ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,26 +9,14 @@ on: tags: - '*' -env: - BUNDLE_WITHOUT: development:test:system_tests - jobs: - deploy: - name: 'deploy to forge' - runs-on: ubuntu-latest - if: github.repository_owner == 'root-expert' - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - bundler-cache: true - - name: Build and Deploy - env: - # Configure secrets here: - # https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets - BLACKSMITH_FORGE_USERNAME: '${{ secrets.PUPPET_FORGE_USERNAME }}' - BLACKSMITH_FORGE_API_KEY: '${{ secrets.PUPPET_FORGE_API_KEY }}' - run: bundle exec rake module:push + release: + name: Release + uses: voxpupuli/gha-puppet/.github/workflows/release.yml@v1 + with: + allowed_owner: 'root-expert' + secrets: + # Configure secrets here: + # https://docs.github.com/en/actions/security-guides/encrypted-secrets + username: ${{ secrets.PUPPET_FORGE_USERNAME }} + api_key: ${{ secrets.PUPPET_FORGE_API_KEY }} diff --git a/.msync.yml b/.msync.yml index ab186de..968a936 100644 --- a/.msync.yml +++ b/.msync.yml @@ -2,4 +2,4 @@ # Managed by modulesync - DO NOT EDIT # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ -modulesync_config_version: '5.0.1' +modulesync_config_version: '5.2.0' diff --git a/Dockerfile b/Dockerfile index e3cf307..8dd82d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /opt/puppet # https://github.com/puppetlabs/puppet/blob/06ad255754a38f22fb3a22c7c4f1e2ce453d01cb/lib/puppet/provider/service/runit.rb#L39 RUN mkdir -p /etc/sv -ARG PUPPET_VERSION="~> 6.0" +ARG PUPPET_GEM_VERSION="~> 6.0" ARG PARALLEL_TEST_PROCESSORS=4 # Cache gems diff --git a/Gemfile b/Gemfile index d14ad99..342c26f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,13 @@ # Managed by modulesync - DO NOT EDIT # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ -source ENV['GEM_SOURCE'] || "https://rubygems.org" +source ENV['GEM_SOURCE'] || 'https://rubygems.org' group :test do - gem 'voxpupuli-test', '~> 4.0', :require => false + gem 'voxpupuli-test', '~> 5.0', :require => false gem 'coveralls', :require => false gem 'simplecov-console', :require => false gem 'puppet_metadata', '~> 1.0', :require => false - gem 'puppet-lint-param-docs', :require => false gem 'net_http_unix', :require => false end @@ -30,7 +29,7 @@ end gem 'rake', :require => false gem 'facter', ENV['FACTER_GEM_VERSION'], :require => false, :groups => [:test] -puppetversion = ENV['PUPPET_VERSION'] || '>= 6.0' +puppetversion = ENV['PUPPET_GEM_VERSION'] || '>= 6.0' gem 'puppet', puppetversion, :require => false, :groups => [:test] # vim: syntax=ruby diff --git a/Rakefile b/Rakefile index 80b799d..f92f051 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,7 @@ # Managed by modulesync - DO NOT EDIT # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ -# Attempt to load voxupuli-test (which pulls in puppetlabs_spec_helper), +# Attempt to load voxpupuli-test (which pulls in puppetlabs_spec_helper), # otherwise attempt to load it directly. begin require 'voxpupuli/test/rake' From 570f64c25ac3868315e682dc33c3733ec8f56dae Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 17:14:25 +0200 Subject: [PATCH 09/18] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5a455bd..2d9e9dc 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![Build Status](https://github.com/root-expert/puppet-snap/workflows/CI/badge.svg)](https://github.com/root-expert/puppet-snap/actions?query=workflow%3ACI) [![Release](https://github.com/root-expert/puppet-snap/actions/workflows/release.yml/badge.svg)](https://github.com/root-expert/puppet-snap/actions/workflows/release.yml) -[![Puppet Forge](https://img.shields.io/puppetforge/v/puppet/snap.svg)](https://forge.puppetlabs.com/puppet/snap) -[![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/puppet/snap.svg)](https://forge.puppetlabs.com/puppet/snap) -[![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/puppet/snap.svg)](https://forge.puppetlabs.com/puppet/snap) -[![Puppet Forge - scores](https://img.shields.io/puppetforge/f/puppet/snap.svg)](https://forge.puppetlabs.com/puppet/snap) +[![Puppet Forge](https://img.shields.io/puppetforge/v/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) +[![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) +[![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) +[![Puppet Forge - scores](https://img.shields.io/puppetforge/f/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) [![puppetmodule.info docs](http://www.puppetmodule.info/images/badge.png)](http://www.puppetmodule.info/m/puppet-snap) [![Apache-2 License](https://img.shields.io/github/license/root-expert/puppet-snap.svg)](LICENSE) @@ -40,7 +40,7 @@ the [Snapd REST API](https://snapcraft.io/docs/snapd-api) for managing snaps. To install Snap and the core package: ```puppet -class { 'snap': } +include snap ``` If you are using a RedHat family OS you need to additionally install [puppet-epel](https://github.com/voxpupuli/puppet-epel) From 829a07cd82a09d84323c5ce7d73ba6ddae9ffbac Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 17:24:55 +0200 Subject: [PATCH 10/18] Change user for GithubChangelogGenerator --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index f92f051..52a5be2 100644 --- a/Rakefile +++ b/Rakefile @@ -51,7 +51,7 @@ begin config.future_release = "v#{metadata.version}" if metadata.version =~ /^\d+\.\d+.\d+$/ config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\nEach new release typically also includes the latest modulesync defaults.\nThese should not affect the functionality of the module." config.exclude_labels = %w{duplicate question invalid wontfix wont-fix modulesync skip-changelog} - config.user = 'voxpupuli' + config.user = 'root-expert' config.project = metadata.metadata['name'] end From 54c14de625fa39f1c8dd6ed0683dac6b84b791eb Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 17:25:13 +0200 Subject: [PATCH 11/18] Release 1.0.0 --- CHANGELOG.md | 17 +++++++++++++++++ metadata.json | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..eba5c43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. +Each new release typically also includes the latest modulesync defaults. +These should not affect the functionality of the module. + +## [v1.0.0](https://github.com/root-expert/puppet-snap/tree/v1.0.0) (2022-03-26) + +[Full Changelog](https://github.com/root-expert/puppet-snap/compare/613d2068319841ea636da5f22c16665311001304...v1.0.0) + +**Implemented enhancements:** + +- Add snap\_conf resource/Add CentOS 9 [\#4](https://github.com/root-expert/puppet-snap/pull/4) ([root-expert](https://github.com/root-expert)) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/metadata.json b/metadata.json index 1d26f33..8be46bb 100644 --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { - "name": "puppet-snap", - "version": "0.1.0-rc0", + "name": "rootexpert-snap", + "version": "1.0.0", "author": "root-expert", "summary": "Puppet module for installing Snap and managing snap packages.", "license": "Apache-2.0", From 63a6955ac65172194363badb44b70f0b2265c1e0 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 17:26:48 +0200 Subject: [PATCH 12/18] [blacksmith] Bump version to 1.0.1-rc0 --- metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata.json b/metadata.json index 8be46bb..4e8b5ca 100644 --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { "name": "rootexpert-snap", - "version": "1.0.0", + "version": "1.0.1-rc0", "author": "root-expert", "summary": "Puppet module for installing Snap and managing snap packages.", "license": "Apache-2.0", From 908f9f9cf0e1d22622d43dbdb54a88280ae3b856 Mon Sep 17 00:00:00 2001 From: Christos Gkoumas Date: Sat, 26 Mar 2022 18:04:35 +0200 Subject: [PATCH 13/18] Update README with proper puppet forge links --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2d9e9dc..60d44aa 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![Build Status](https://github.com/root-expert/puppet-snap/workflows/CI/badge.svg)](https://github.com/root-expert/puppet-snap/actions?query=workflow%3ACI) [![Release](https://github.com/root-expert/puppet-snap/actions/workflows/release.yml/badge.svg)](https://github.com/root-expert/puppet-snap/actions/workflows/release.yml) -[![Puppet Forge](https://img.shields.io/puppetforge/v/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) -[![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) -[![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) -[![Puppet Forge - scores](https://img.shields.io/puppetforge/f/root-expert/snap.svg)](https://forge.puppet.com/root-expert/snap) -[![puppetmodule.info docs](http://www.puppetmodule.info/images/badge.png)](http://www.puppetmodule.info/m/puppet-snap) +[![Puppet Forge](https://img.shields.io/puppetforge/v/rootexpert/snap.svg)](https://forge.puppet.com/modules/rootexpert/snap) +[![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/rootexpert/snap.svg)](https://forge.puppet.com/modules/rootexpert/snap) +[![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/rootexpert/snap.svg)](https://forge.puppet.com/modules/rootexpert/snap) +[![Puppet Forge - scores](https://img.shields.io/puppetforge/f/rootexpert/snap.svg)](https://forge.puppet.com/modules/rootexpert/snap) +[![puppetmodule.info docs](http://www.puppetmodule.info/images/badge.png)](http://www.puppetmodule.info/m/rootexpert-snap) [![Apache-2 License](https://img.shields.io/github/license/root-expert/puppet-snap.svg)](LICENSE) #### Table of Contents From 1d9228bcb012e6cf757338f5655d6f1706d63bd1 Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 26 Mar 2022 18:10:08 +0200 Subject: [PATCH 14/18] Add Fedora 33/34 support --- data/os/Fedora.yaml | 2 ++ metadata.json | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 data/os/Fedora.yaml diff --git a/data/os/Fedora.yaml b/data/os/Fedora.yaml new file mode 100644 index 0000000..4ab3ddb --- /dev/null +++ b/data/os/Fedora.yaml @@ -0,0 +1,2 @@ +--- +snap::manage_repo: false diff --git a/metadata.json b/metadata.json index 4e8b5ca..8a7cd00 100644 --- a/metadata.json +++ b/metadata.json @@ -42,6 +42,13 @@ "9" ] }, + { + "operatingsystem": "Fedora", + "operatingsystemrelease": [ + "33", + "34" + ] + }, { "operatingsystem": "Debian", "operatingsystemrelease": [ From 10ddeaf7cbb85f4bf1db726aa6dfb61c5896bf3a Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sun, 27 Mar 2022 12:11:24 +0300 Subject: [PATCH 15/18] Use the specified channel when quering for the latest version --- lib/puppet/provider/package/snap.rb | 7 ++++--- spec/unit/puppet/provider/package/snap_spec.rb | 13 ++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index fd90154..5bf1fa0 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -47,13 +47,14 @@ def latest raise Puppet::Error, "Couldn't find latest version" if res['status-code'] != 200 - # Search latest version for the specified channel. If unspecified fallback to stable.c + # Search latest version for the specified channel. If channel is unspecified, fallback to latest/stable channel = if @resource[:install_options].nil? - 'stable' + 'latest/stable' else self.class.parse_channel(@resource[:install_options]) end - selected_channel = res['result'][0]['channels']["latest/#{channel}"] + + selected_channel = res['result'].first&.dig('channels', channel) raise Puppet::Error, "No version in channel #{channel}" unless selected_channel # Return version diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 1699519..693f3eb 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -66,17 +66,24 @@ end context 'querying for latest version' do - it 'with no channel specified returns correct version from stable channel' do + before do allow(PuppetX::Snap::API).to receive(:get).with('/v2/find?name=hello-world').and_return(find_res) + end + it 'with no channel specified returns correct version from latest/stable channel' do expect(provider.latest).to eq('6.4') end it 'with channel specified returns correct version from specified channel' do - resource[:install_options] = ['channel=beta'] - allow(PuppetX::Snap::API).to receive(:get).with('/v2/find?name=hello-world').and_return(find_res) + resource[:install_options] = ['channel=latest/beta'] expect(provider.latest).to eq('6.0') end + + it 'with non-existent channel' do + resource[:install_options] = ['channel=latest/kokolala'] + + expect { provider.latest }.to raise_error(%r{No version in channel latest/kokolala$}) + end end end From 0bff8c9199bc758175f3355f6a2017cf5612ddad Mon Sep 17 00:00:00 2001 From: Spyros Pagkalos Date: Sun, 27 Mar 2022 19:19:19 +0200 Subject: [PATCH 16/18] 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 60d44aa..2eb65ca 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 From e617f0b57220517b4ff1d12c32a87af2dae09b0b Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Sat, 1 Jun 2024 18:15:05 +0300 Subject: [PATCH 17/18] Make provider upgradable; Add logic to correctly switch between channels --- README.md | 12 +++- lib/puppet/provider/package/snap.rb | 59 +++++++++++++++---- spec/acceptance/01_snapd_spec.rb | 37 ++++++++++++ .../unit/puppet/provider/package/snap_spec.rb | 1 + 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2eb65ca..566b7f2 100644 --- a/README.md +++ b/README.md @@ -103,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) diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 635d536..701b5be 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -13,35 +13,68 @@ " commands snap_cmd: '/usr/bin/snap' - has_feature :installable, :versionable, :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 + Puppet.info('called instances') @installed_snaps ||= installed_snaps + Puppet.info("installed_snaps = #{@installed_snaps}") @installed_snaps.map do |snap| new(name: snap['name'], ensure: snap['tracking-channel'], provider: 'snap') end end def query - 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 - - def latest - query&.get(:ensure) + Puppet.info('called query') + installed = @installed_snaps&.find { |it| it.name == @resource['name'] } + Puppet.info("installed #{installed}") + { ensure: installed.ensure, name: @resource[:name] } if installed end def install - modify_snap('install') + Puppet.info('called install') + current_ensure = query&.dig(:ensure) + current_ensure ||= :absent + + Puppet.info("current_ensure = #{current_ensure}") + # Refresh the snap if we changed the channel + if current_ensure != @resource[:ensure] && current_ensure != :absent + Puppet.info('modify snap') + modify_snap('refresh') # Refresh will switch the channel AND trigger a refresh immediately. TODO Implement switch? + else + Puppet.info('install snap') + modify_snap('install') + end end def update - modify_snap('switch') + install + end + + def latest + Puppet.info('called 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 + + Puppet.info("channel = #{channel}") + selected_channel = res['result'].first&.dig('channels', channel) + Puppet.info("selected_channel = #{selected_channel}") + raise Puppet::Error, "No version in channel #{channel}" unless selected_channel + + Puppet.info('Evaluating version') + Puppet.info("version = #{selected_channel['version']}") + # Return version + selected_channel['version'] end def uninstall diff --git a/spec/acceptance/01_snapd_spec.rb b/spec/acceptance/01_snapd_spec.rb index fdf4935..96f6ea2 100644 --- a/spec/acceptance/01_snapd_spec.rb +++ b/spec/acceptance/01_snapd_spec.rb @@ -97,4 +97,41 @@ 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 + + describe 'installs latest/stable when ensure: latest' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => latest, + 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{stable}) + end + end + end end diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 86effc9..329ca57 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -27,6 +27,7 @@ it { is_expected.to be_install_options } it { is_expected.to be_uninstallable } it { is_expected.to be_purgeable } + it { is_expected.to be_upgradable } end context 'should respond to' do From 899f67eaf36a5ced3e0e0b0e6f3e41e5cc94189d Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Wed, 11 Sep 2024 17:55:59 +0300 Subject: [PATCH 18/18] fixup! Make provider upgradable; Add logic to correctly switch between channels --- lib/puppet/provider/package/snap.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 701b5be..1850467 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -27,6 +27,7 @@ def self.instances def query Puppet.info('called query') + Puppet.info("@installed_snaps = #{installed_snaps}") installed = @installed_snaps&.find { |it| it.name == @resource['name'] } Puppet.info("installed #{installed}") { ensure: installed.ensure, name: @resource[:name] } if installed