Skip to content

Commit 80ced07

Browse files
committed
Make provider holdable
1 parent 60f45f5 commit 80ced07

File tree

3 files changed

+144
-19
lines changed

3 files changed

+144
-19
lines changed

lib/puppet/provider/package/snap.rb

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'date'
34
require 'puppet/provider/package'
45
require 'puppet_x/snap/api'
56

@@ -13,14 +14,16 @@
1314
"
1415

1516
commands snap_cmd: '/usr/bin/snap'
16-
has_feature :installable, :versionable, :install_options, :uninstallable, :purgeable, :upgradeable
17+
has_feature :installable, :versionable, :install_options, :uninstallable, :purgeable, :upgradeable, :holdable
1718
confine feature: %i[net_http_unix_lib snapd_socket]
1819

1920
mk_resource_methods
2021

2122
def self.instances
2223
installed_snaps.map do |snap|
23-
new(name: snap['name'], ensure: snap['tracking-channel'], provider: 'snap')
24+
mark = snap['refresh-inhibit'].nil? ? 'none' : 'hold'
25+
Puppet.info("refresh-inhibit = mark")
26+
new(name: snap['name'], ensure: snap['tracking-channel'], mark: mark, provider: 'snap')
2427
end
2528
end
2629

@@ -56,6 +59,17 @@ def purge
5659
modify_snap('remove', ['purge'])
5760
end
5861

62+
def hold
63+
Puppet.info('called hold')
64+
Puppet.info('')
65+
modify_snap('hold')
66+
end
67+
68+
def unhold
69+
Puppet.info('called unhold')
70+
modify_snap('unhold')
71+
end
72+
5973
def modify_snap(action, options = @resource[:install_options])
6074
body = self.class.generate_request(action, determine_channel, options)
6175
response = PuppetX::Snap::API.post("/v2/snaps/#{@resource[:name]}", body)
@@ -73,6 +87,7 @@ def determine_channel
7387
def self.generate_request(action, channel, options)
7488
request = { 'action' => action }
7589
request['channel'] = channel unless channel.nil?
90+
request['hold-level'] = 'general' if action.equal?('hold')
7691

7792
if options
7893
# classic, devmode and jailmode params are only
@@ -84,9 +99,17 @@ def self.generate_request(action, channel, options)
8499
request['jailmode'] = true if options.include?('jailmode')
85100
when 'remove'
86101
request['purge'] = true if options.include?('purge')
102+
when 'hold'
103+
time = parse_time_from_options(options)
104+
request['time'] = time
87105
end
106+
elsif action.equal?('hold')
107+
# If no options defined assume hold time forever
108+
request['time'] = 'forever'
88109
end
89110

111+
Puppet.info("request = #{request}")
112+
90113
request
91114
end
92115

@@ -100,6 +123,19 @@ def self.channel_from_ensure(value)
100123
end
101124
end
102125

126+
def self.parse_time_from_options(options)
127+
time = options&.find { |opt| %r{hold_time} =~ opt }&.split('=')&.last
128+
129+
# Assume forever if not hold_time was specified
130+
return 'forever' if time.nil? || time.equal?('forever')
131+
132+
begin
133+
DateTime.parse(time).rfc3339
134+
rescue Date::Error
135+
raise Puppet::Error, 'Date not in correct format.'
136+
end
137+
end
138+
103139
def self.channel_from_options(options)
104140
options&.find { |e| %r{channel} =~ e }&.split('=')&.last&.tap do |ch|
105141
Puppet.warning("Install option 'channel' is deprecated, use ensure => '#{ch}' instead.")
@@ -110,6 +146,6 @@ def self.installed_snaps
110146
res = PuppetX::Snap::API.get('/v2/snaps')
111147
raise Puppet::Error, "Could not find installed snaps (code: #{res['status-code']})" unless [200, 404].include?(res['status-code'])
112148

113-
res['status-code'] == 200 ? res['result'].map { |hash| hash.slice('name', 'tracking-channel') } : []
149+
res['status-code'] == 200 ? res['result'].map { |hash| hash.slice('name', 'tracking-channel', 'refresh-inhibit') } : []
114150
end
115151
end

spec/acceptance/01_snapd_spec.rb

+104-16
Original file line numberDiff line numberDiff line change
@@ -96,35 +96,123 @@
9696
end
9797
end
9898
end
99-
end
10099

101-
describe 'purges the package' do
102-
let(:manifest) do
103-
<<-PUPPET
100+
describe 'holds the package (prevents refresh)' do
101+
let(:manifest) do
102+
<<-PUPPET
103+
package { 'hello-world':
104+
ensure => 'latest/beta',
105+
mark => 'hold',
106+
provider => 'snap',
107+
}
108+
PUPPET
109+
end
110+
111+
it_behaves_like 'an idempotent resource'
112+
113+
describe command('snap list --unicode=never --color=never') do
114+
its(:stdout) do
115+
is_expected.to match(%r{hello-world})
116+
is_expected.to match(%r{beta})
117+
is_expected.to match(%r{held})
118+
end
119+
end
120+
end
121+
122+
describe 'can change channel while held' do
123+
let(:manifest) do
124+
<<-PUPPET
125+
package { 'hello-world':
126+
ensure => 'latest/candidate',
127+
mark => 'hold',
128+
provider => 'snap',
129+
}
130+
PUPPET
131+
end
132+
133+
it_behaves_like 'an idempotent resource'
134+
135+
describe command('snap list --unicode=never --color=never') do
136+
its(:stdout) do
137+
is_expected.to match(%r{hello-world})
138+
is_expected.to match(%r{candidate})
139+
is_expected.to match(%r{held})
140+
end
141+
end
142+
end
143+
144+
describe 'hold until specified date' do
145+
let(:manifest) do
146+
<<-PUPPET
147+
package { 'hello-world':
148+
ensure => 'latest/candidate',
149+
mark => 'hold',
150+
install_options => 'hold_time=2025-10-10', # Non RFC3339, it should be parsed correctly
151+
provider => 'snap',
152+
}
153+
PUPPET
154+
end
155+
156+
it_behaves_like 'an idempotent resource'
157+
158+
describe command('snap list --unicode=never --color=never') do
159+
its(:stdout) do
160+
is_expected.to match(%r{hello-world})
161+
is_expected.to match(%r{candidate})
162+
is_expected.to match(%r{held})
163+
end
164+
end
165+
end
166+
167+
describe 'unholds the package' do
168+
let(:manifest) do
169+
<<-PUPPET
170+
package { 'hello-world':
171+
ensure => 'latest/candidate',
172+
provider => 'snap',
173+
}
174+
PUPPET
175+
end
176+
177+
it_behaves_like 'an idempotent resource'
178+
179+
describe command('snap list --unicode=never --color=never') do
180+
its(:stdout) do
181+
is_expected.to match(%r{hello-world})
182+
is_expected.to match(%r{candidate})
183+
is_expected.not_to match(%r{held})
184+
end
185+
end
186+
end
187+
188+
describe 'purges the package' do
189+
let(:manifest) do
190+
<<-PUPPET
104191
package { 'hello-world':
105192
ensure => purged,
106193
provider => snap,
107194
}
108-
PUPPET
109-
end
195+
PUPPET
196+
end
110197

111-
it_behaves_like 'an idempotent resource'
198+
it_behaves_like 'an idempotent resource'
112199

113-
describe command('snap list --unicode=never --color=never') do
114-
its(:stdout) { is_expected.not_to match(%r{hello-world}) }
200+
describe command('snap list --unicode=never --color=never') do
201+
its(:stdout) { is_expected.not_to match(%r{hello-world}) }
202+
end
115203
end
116-
end
117204

118-
# rubocop:disable RSpec/EmptyExampleGroup
119-
describe 'Raises error when ensure => latest' do
120-
manifest = <<-PUPPET
205+
# rubocop:disable RSpec/EmptyExampleGroup
206+
describe 'Raises error when ensure => latest' do
207+
manifest = <<-PUPPET
121208
package { 'hello-world':
122209
ensure => latest,
123210
provider => snap,
124211
}
125-
PUPPET
212+
PUPPET
126213

127-
apply_manifest(manifest, expect_failures: true)
214+
apply_manifest(manifest, expect_failures: true)
215+
end
216+
# rubocop:enable RSpec/EmptyExampleGroup
128217
end
129-
# rubocop:enable RSpec/EmptyExampleGroup
130218
end

spec/unit/puppet/provider/package/snap_spec.rb

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
it { is_expected.to be_uninstallable }
2929
it { is_expected.to be_purgeable }
3030
it { is_expected.to be_upgradeable }
31+
it { is_expected.to be_holdable }
3132
end
3233

3334
context 'should respond to' do

0 commit comments

Comments
 (0)