From 9f6a313d65cf7db6f2631911054be4ea47850d8d Mon Sep 17 00:00:00 2001 From: Christos Papageorgiou Date: Fri, 13 Sep 2024 19:05:24 +0300 Subject: [PATCH] Make provider holdable --- lib/puppet/provider/package/snap.rb | 46 ++++++- spec/acceptance/01_snapd_spec.rb | 120 +++++++++++++++--- .../unit/puppet/provider/package/snap_spec.rb | 1 + 3 files changed, 147 insertions(+), 20 deletions(-) diff --git a/lib/puppet/provider/package/snap.rb b/lib/puppet/provider/package/snap.rb index 30b3dc7..6623b62 100644 --- a/lib/puppet/provider/package/snap.rb +++ b/lib/puppet/provider/package/snap.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'date' require 'puppet/provider/package' require 'puppet_x/snap/api' @@ -13,22 +14,27 @@ " commands snap_cmd: '/usr/bin/snap' - has_feature :installable, :versionable, :install_options, :uninstallable, :purgeable, :upgradeable + has_feature :installable, :versionable, :install_options, :uninstallable, :purgeable, :upgradeable, :holdable confine feature: %i[net_http_unix_lib snapd_socket] mk_resource_methods def self.instances installed_snaps.map do |snap| - new(name: snap['name'], ensure: snap['tracking-channel'], provider: 'snap') + mark = snap['hold'].nil? ? 'none' : 'hold' + Puppet.info("refresh-inhibit = #{mark}") + new(name: snap['name'], ensure: snap['tracking-channel'], mark: mark, hold_time: snap['hold'], provider: 'snap') end end def query - { ensure: @property_hash[:ensure], name: @resource[:name] } unless @property_hash.empty? + Puppet.info('called query') + Puppet.info("@property_hash #{@property_hash}") + { ensure: @property_hash[:ensure], name: @resource[:name], mark: @property_hash[:mark] } unless @property_hash.empty? end def install + Puppet.info('called install') current_ensure = query&.dig(:ensure) # Refresh the snap if we changed the channel @@ -56,6 +62,16 @@ def purge modify_snap('remove', ['purge']) end + def hold + Puppet.info('called hold') + modify_snap('hold') unless @property_hash[:mark].equal?('hold') && @property_hash[:hold_time] != @resource[:install_options] + end + + def unhold + Puppet.info('called unhold') + modify_snap('unhold') unless @property_hash[:mark].equal?('none') + end + 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) @@ -73,6 +89,7 @@ def determine_channel def self.generate_request(action, channel, options) request = { 'action' => action } request['channel'] = channel unless channel.nil? + request['hold-level'] = 'general' if action.equal?('hold') if options # classic, devmode and jailmode params are only @@ -84,9 +101,17 @@ def self.generate_request(action, channel, options) request['jailmode'] = true if options.include?('jailmode') when 'remove' request['purge'] = true if options.include?('purge') + when 'hold' + time = parse_time_from_options(options) + request['time'] = time end + elsif action.equal?('hold') + # If no options defined assume hold time forever + request['time'] = 'forever' end + Puppet.info("request = #{request}") + request end @@ -100,6 +125,19 @@ def self.channel_from_ensure(value) end end + def self.parse_time_from_options(options) + time = options&.find { |opt| %r{hold_time} =~ opt }&.split('=')&.last + + # Assume forever if not hold_time was specified + return 'forever' if time.nil? || time.equal?('forever') + + begin + DateTime.parse(time).rfc3339 + rescue Date::Error + raise Puppet::Error, 'Date not in correct format.' + end + end + 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.") @@ -110,6 +148,6 @@ 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']) - res['status-code'] == 200 ? res['result'].map { |hash| hash.slice('name', 'tracking-channel') } : [] + res['status-code'] == 200 ? res['result'].map { |hash| hash.slice('name', 'tracking-channel', 'hold') } : [] end end diff --git a/spec/acceptance/01_snapd_spec.rb b/spec/acceptance/01_snapd_spec.rb index 53de4c7..71eb11d 100644 --- a/spec/acceptance/01_snapd_spec.rb +++ b/spec/acceptance/01_snapd_spec.rb @@ -96,35 +96,123 @@ end end end - end - describe 'purges the package' do - let(:manifest) do - <<-PUPPET + describe 'holds the package (prevents refresh)' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => 'latest/beta', + mark => 'hold', + 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}) + is_expected.to match(%r{held}) + end + end + end + + describe 'can change channel while held' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => 'latest/candidate', + mark => 'hold', + 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}) + is_expected.to match(%r{held}) + end + end + end + + describe 'hold until specified date' do + let(:manifest) do + <<-PUPPET + package { 'hello-world': + ensure => 'latest/candidate', + mark => 'hold', + install_options => 'hold_time=2025-10-10', # Non RFC3339, it should be parsed correctly + 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}) + is_expected.to match(%r{held}) + end + end + end + + describe 'unholds the package' 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}) + is_expected.not_to match(%r{held}) + end + end + end + + describe 'purges the package' do + let(:manifest) do + <<-PUPPET package { 'hello-world': ensure => purged, provider => snap, } - PUPPET - end + PUPPET + end - it_behaves_like 'an idempotent resource' + 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}) } + describe command('snap list --unicode=never --color=never') do + its(:stdout) { is_expected.not_to match(%r{hello-world}) } + end end - end - # rubocop:disable RSpec/EmptyExampleGroup - describe 'Raises error when ensure => latest' do - manifest = <<-PUPPET + # rubocop:disable RSpec/EmptyExampleGroup + describe 'Raises error when ensure => latest' do + manifest = <<-PUPPET package { 'hello-world': ensure => latest, provider => snap, } - PUPPET + PUPPET - apply_manifest(manifest, expect_failures: true) + apply_manifest(manifest, expect_failures: true) + end + # rubocop:enable RSpec/EmptyExampleGroup end - # rubocop:enable RSpec/EmptyExampleGroup end diff --git a/spec/unit/puppet/provider/package/snap_spec.rb b/spec/unit/puppet/provider/package/snap_spec.rb index 38af822..404d7c4 100644 --- a/spec/unit/puppet/provider/package/snap_spec.rb +++ b/spec/unit/puppet/provider/package/snap_spec.rb @@ -28,6 +28,7 @@ it { is_expected.to be_uninstallable } it { is_expected.to be_purgeable } it { is_expected.to be_upgradeable } + it { is_expected.to be_holdable } end context 'should respond to' do