Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 90 additions & 3 deletions lib/puppet/provider/package/snap.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'date'
require 'puppet/provider/package'
require 'puppet_x/snap/api'

Expand All @@ -13,22 +14,57 @@
"

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.prefetch(resources)
Puppet.info('Called prefetch')
# Build a hash of name => install_options from the catalog
desired_options = resources.transform_values { |res| res[:install_options] }

installed_snaps.each do |snap|
resource = resources[snap['name']]
next unless resource

current_hold_time = snap['hold']
desired_hold_time = parse_time_from_options(desired_options[snap['name']])

# Determine the appropriate mark
mark = if should_change_hold?(desired_hold_time, current_hold_time)
:none # force re-hold
else
:hold
end

provider = new(
name: snap['name'],
ensure: snap['tracking-channel'],
mark: mark,
hold_time: current_hold_time,
provider: 'snap'
)

resource.provider = provider
end
end

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("name = #{snap['name']}, refresh-inhibit = #{mark}")
new(name: snap['name'], ensure: snap['tracking-channel'], mark: mark, hold_time: snap['hold'], provider: 'snap')
end
end

def query
Puppet.info('called query')
{ ensure: @property_hash[:ensure], name: @resource[:name] } unless @property_hash.empty?
end

def install
Puppet.info('called install')
current_ensure = query&.dig(:ensure)

# Refresh the snap if we changed the channel
Expand Down Expand Up @@ -56,6 +92,19 @@ def purge
modify_snap('remove', ['purge'])
end

def hold
Puppet.info('called hold')
Puppet.info("@property_hash = #{@property_hash}")
Puppet.info("install_options = #{@resource[:install_options]}")
modify_snap('hold')
# @property_hash[:mark] = 'hold'
end

def unhold
Puppet.info('called unhold')
modify_snap('unhold')
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)
Expand All @@ -73,6 +122,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
Expand All @@ -84,9 +134,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 = self.class.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

Expand All @@ -100,6 +158,35 @@ 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.should_change_hold?(options, current_hold_time)
should_hold_time = self.class.parse_time_from_options(options)
# current_hold_time = @property_hash[:hold_time]

# if current hold time is nil we are fresh holding this snap
return true if current_hold_time.nil?

parsed_hold_time = DateTime.parse(current_hold_time)
# If the hold time is more than 100 years, assume "forever"
current_hold_time = 'forever' if (parsed_hold_time - DateTime.now).to_i > 365 * 100

Puppet.info("should = #{should_hold_time}, current = #{current_hold_time}")
Puppet.info("equal = #{current_hold_time == should_hold_time}")
current_hold_time != should_hold_time
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.")
Expand All @@ -110,6 +197,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
39 changes: 0 additions & 39 deletions metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,9 @@

],
"operatingsystem_support": [
{
"operatingsystem": "RedHat",
"operatingsystemrelease": [
"9"
]
},
{
"operatingsystem": "CentOS",
"operatingsystemrelease": [
"9"
]
},
{
"operatingsystem": "OracleLinux",
"operatingsystemrelease": [
"9"
]
},
{
"operatingsystem": "Scientific",
"operatingsystemrelease": [
"9"
]
},
{
"operatingsystem": "Fedora",
"operatingsystemrelease": [
"40"
]
},
{
"operatingsystem": "Debian",
"operatingsystemrelease": [
"11",
"12"
]
},
{
"operatingsystem": "Ubuntu",
"operatingsystemrelease": [
"20.04",
"22.04",
"24.04"
]
}
Expand Down
120 changes: 104 additions & 16 deletions spec/acceptance/01_snapd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
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}) }

Check failure on line 56 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / Puppet 7 - Ubuntu 24.04

snapd class package resource uninstalls package Command "snap list --unicode=never --color=never" stdout is expected not to match /hello-world/ Failure/Error: its(:stdout) { is_expected.not_to match(%r{hello-world}) } expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" not to match /hello-world/ Diff: @@ -1 +1,4 @@ -/hello-world/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 56 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / OpenVox 7 - Ubuntu 24.04

snapd class package resource uninstalls package Command "snap list --unicode=never --color=never" stdout is expected not to match /hello-world/ Failure/Error: its(:stdout) { is_expected.not_to match(%r{hello-world}) } expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" not to match /hello-world/ Diff: @@ -1 +1,4 @@ -/hello-world/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 56 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / OpenVox 8 - Ubuntu 24.04

snapd class package resource uninstalls package Command "snap list --unicode=never --color=never" stdout is expected not to match /hello-world/ Failure/Error: its(:stdout) { is_expected.not_to match(%r{hello-world}) } expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" not to match /hello-world/ Diff: @@ -1 +1,4 @@ -/hello-world/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 56 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / Puppet 8 - Ubuntu 24.04

snapd class package resource uninstalls package Command "snap list --unicode=never --color=never" stdout is expected not to match /hello-world/ Failure/Error: its(:stdout) { is_expected.not_to match(%r{hello-world}) } expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" not to match /hello-world/ Diff: @@ -1 +1,4 @@ -/hello-world/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd
end
end

Expand All @@ -70,7 +70,7 @@
it_behaves_like 'an idempotent resource'

describe command('snap list --unicode=never --color=never') do
its(:stdout) do

Check failure on line 73 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / Puppet 7 - Ubuntu 24.04

snapd class package resource installs package with specified version Command "snap list --unicode=never --color=never" stdout is expected to match /candidate/ Failure/Error: is_expected.to match(%r{candidate}) expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" to match /candidate/ Diff: @@ -1 +1,4 @@ -/candidate/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 73 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / OpenVox 7 - Ubuntu 24.04

snapd class package resource installs package with specified version Command "snap list --unicode=never --color=never" stdout is expected to match /candidate/ Failure/Error: is_expected.to match(%r{candidate}) expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" to match /candidate/ Diff: @@ -1 +1,4 @@ -/candidate/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 73 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / OpenVox 8 - Ubuntu 24.04

snapd class package resource installs package with specified version Command "snap list --unicode=never --color=never" stdout is expected to match /candidate/ Failure/Error: is_expected.to match(%r{candidate}) expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" to match /candidate/ Diff: @@ -1 +1,4 @@ -/candidate/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd

Check failure on line 73 in spec/acceptance/01_snapd_spec.rb

View workflow job for this annotation

GitHub Actions / Puppet / Puppet 8 - Ubuntu 24.04

snapd class package resource installs package with specified version Command "snap list --unicode=never --color=never" stdout is expected to match /candidate/ Failure/Error: is_expected.to match(%r{candidate}) expected "Name Version Rev Tracking Publisher Notes\ncore 16-2.61.4-20...stable canonical** -\nsnapd 2.68.5 24718 latest/stable canonical** snapd\n" to match /candidate/ Diff: @@ -1 +1,4 @@ -/candidate/ +Name Version Rev Tracking Publisher Notes +core 16-2.61.4-20250508 17212 latest/stable canonical** core +hello-world 6.4 29 latest/stable canonical** - +snapd 2.68.5 24718 latest/stable canonical** snapd
is_expected.to match(%r{hello-world})
is_expected.to match(%r{candidate})
end
Expand All @@ -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 info --unicode=never --color=never --abs-time hello-world') do
its(:stdout) do
is_expected.to match(%r{name:\s+hello-world})
is_expected.to match(%r{tracking:\s+latest/beta})
is_expected.to match(%r{hold:\s+forever})
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 info --unicode=never --color=never --abs-time hello-world') do
its(:stdout) do
is_expected.to match(%r{name:\s+hello-world})
is_expected.to match(%r{tracking:\s+latest/candidate})
is_expected.to match(%r{hold:\s+forever})
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 info --unicode=never --color=never --abs-time hello-world') do
its(:stdout) do
is_expected.to match(%r{name:\s+hello-world})
is_expected.to match(%r{tracking:\s+latest/candidate})
is_expected.to match(%r{hold:\s+2025-10-10T03:00:00})
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 info --unicode=never --color=never --abs-time hello-world') do
its(:stdout) do
is_expected.to match(%r{name:\s+hello-world})
is_expected.to match(%r{tracking:\s+latest/candidate})
is_expected.not_to match(%r{hold:.*})
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
1 change: 1 addition & 0 deletions spec/unit/puppet/provider/package/snap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading