Skip to content

Commit 3e0fe7f

Browse files
committed
(puppetlabsGH-1934) Add support for selective sync of plugins to target nodes
* **Added support for selective sync of plugins to target** ([puppetlabs#1934](puppetlabs#1934)) !feature This feature allows a plan author to specificy what plugins need to be synced to the target node. You can do this by add an array of module names to the options parameter. Here is an example: apply($target, ‘required_modules => [‘stdlib’] ) { notice 'Hallo' } The apply block here ony sync’s the stdlib module to the target. The same feature also works on apply_prep functions. Here is an example: apply_prep($target, ‘required_modules => [‘stdlib’] ) When no required modules are specified, *ALL* plugins from *ALL* modules are synced.
1 parent 3826576 commit 3e0fe7f

File tree

4 files changed

+118
-24
lines changed

4 files changed

+118
-24
lines changed

bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
# > **Note:** Not available in apply block
1414
Puppet::Functions.create_function(:apply_prep) do
1515
# @param targets A pattern or array of patterns identifying a set of targets.
16+
# @param options Options hash.
17+
# @option options [Array] _required_modules An array of modules to sync to the target.
1618
# @example Prepare targets by name.
1719
# apply_prep('target1,target2')
1820
dispatch :apply_prep do
1921
param 'Boltlib::TargetSpec', :targets
22+
optional_param 'Hash[String, Data]', :options
2023
end
2124

2225
def script_compiler
@@ -60,18 +63,34 @@ def executor
6063
@executor ||= Puppet.lookup(:bolt_executor)
6164
end
6265

63-
def apply_prep(target_spec)
66+
def apply_prep(target_spec, options = {})
6467
unless Puppet[:tasks]
6568
raise Puppet::ParseErrorWithIssue
6669
.from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
6770
end
6871

72+
options = options.transform_keys { |k| k.sub(/^_/, '').to_sym }
73+
6974
applicator = Puppet.lookup(:apply_executor)
7075

7176
executor.report_function_call(self.class.name)
7277

7378
targets = inventory.get_targets(target_spec)
7479

80+
required_modules = options[:required_modules].nil? ? nil : Array(options[:required_modules])
81+
if required_modules&.any?
82+
Puppet.debug("Syncing only required modules: #{required_modules.join(',')}.")
83+
end
84+
85+
# Gather facts, including custom facts
86+
plugins = applicator.build_plugin_tarball do |mod|
87+
next unless required_modules.nil? || required_modules.include?(mod.name)
88+
search_dirs = []
89+
search_dirs << mod.plugins if mod.plugins?
90+
search_dirs << mod.pluginfacts if mod.pluginfacts?
91+
search_dirs
92+
end
93+
7594
executor.log_action('install puppet and gather facts', targets) do
7695
executor.without_default_logging do
7796
# Skip targets that include the puppet-agent feature, as we know an agent will be available.
@@ -109,14 +128,6 @@ def apply_prep(target_spec)
109128
need_install_targets.each { |target| set_agent_feature(target) }
110129
end
111130

112-
# Gather facts, including custom facts
113-
plugins = applicator.build_plugin_tarball do |mod|
114-
search_dirs = []
115-
search_dirs << mod.plugins if mod.plugins?
116-
search_dirs << mod.pluginfacts if mod.pluginfacts?
117-
search_dirs
118-
end
119-
120131
task = applicator.custom_facts_task
121132
arguments = { 'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins) }
122133
results = executor.run_task(targets, task, arguments)

bolt-modules/boltlib/spec/functions/apply_prep_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,28 @@
217217
end
218218
end
219219

220+
context 'with required_modules specified' do
221+
let(:hostnames) { %w[foo bar] }
222+
let(:targets) { hostnames.map { |h| inventory.get_target(h) } }
223+
let(:fact) { { 'osfamily' => 'none' } }
224+
let(:custom_facts_task) { Bolt::Task.new('custom_facts_task') }
225+
226+
before(:each) do
227+
applicator.stubs(:build_plugin_tarball).returns(:tarball)
228+
applicator.stubs(:custom_facts_task).returns(custom_facts_task)
229+
targets.each { |target| inventory.set_feature(target, 'puppet-agent') }
230+
end
231+
232+
it 'only uses required plugins' do
233+
facts = Bolt::ResultSet.new(targets.map { |t| Bolt::Result.new(t, value: fact) })
234+
executor.expects(:run_task).with(targets, custom_facts_task, includes('plugins')).returns(facts)
235+
236+
Puppet.expects(:debug).at_least(1)
237+
Puppet.expects(:debug).with("Syncing only required modules: non-existing-module.")
238+
is_expected.to run.with_params(hostnames.join(','), 'required_modules' => ['non-existing-module']).and_return(nil)
239+
end
240+
end
241+
220242
context 'without tasks enabled' do
221243
let(:tasks_enabled) { false }
222244
it 'fails and reports that apply_prep is not available' do

lib/bolt/applicator.rb

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ def initialize(inventory, executor, modulepath, plugin_dirs, project,
1818
pdb_client, hiera_config, max_compiles, apply_settings)
1919
# lazy-load expensive gem code
2020
require 'concurrent'
21-
2221
@inventory = inventory
2322
@executor = executor
2423
@modulepath = modulepath || []
@@ -30,17 +29,6 @@ def initialize(inventory, executor, modulepath, plugin_dirs, project,
3029

3130
@pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
3231
@logger = Logging.logger[self]
33-
@plugin_tarball = Concurrent::Delay.new do
34-
build_plugin_tarball do |mod|
35-
search_dirs = []
36-
search_dirs << mod.plugins if mod.plugins?
37-
search_dirs << mod.pluginfacts if mod.pluginfacts?
38-
search_dirs << mod.files if mod.files?
39-
type_files = "#{mod.path}/types"
40-
search_dirs << type_files if File.exist?(type_files)
41-
search_dirs
42-
end
43-
end
4432
end
4533

4634
private def libexec
@@ -188,7 +176,6 @@ def count_statements(ast)
188176

189177
def apply_ast(raw_ast, targets, options, plan_vars = {})
190178
ast = Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
191-
192179
# Serialize as pcore for *Result* objects
193180
plan_vars = Puppet::Pops::Serialization::ToDataConverter.convert(plan_vars,
194181
rich_data: true,
@@ -206,9 +193,26 @@ def apply_ast(raw_ast, targets, options, plan_vars = {})
206193
# This data isn't available on the target config hash
207194
config: @inventory.transport_data_get
208195
}
209-
210196
description = options[:description] || 'apply catalog'
211197

198+
required_modules = options[:required_modules].nil? ? nil : Array(options[:required_modules])
199+
if required_modules&.any?
200+
@logger.debug("Syncing only required modules: #{required_modules.join(',')}.")
201+
end
202+
203+
@plugin_tarball = Concurrent::Delay.new do
204+
build_plugin_tarball do |mod|
205+
next unless required_modules.nil? || required_modules.include?(mod.name)
206+
search_dirs = []
207+
search_dirs << mod.plugins if mod.plugins?
208+
search_dirs << mod.pluginfacts if mod.pluginfacts?
209+
search_dirs << mod.files if mod.files?
210+
type_files = "#{mod.path}/types"
211+
search_dirs << type_files if File.exist?(type_files)
212+
search_dirs
213+
end
214+
end
215+
212216
r = @executor.log_action(description, targets) do
213217
futures = targets.map do |target|
214218
Concurrent::Future.execute(executor: @pool) do
@@ -235,6 +239,7 @@ def apply_ast(raw_ast, targets, options, plan_vars = {})
235239
result
236240
end
237241
else
242+
238243
arguments = {
239244
'catalog' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(catalog),
240245
'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins),

spec/bolt/applicator_spec.rb

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
describe Bolt::Applicator do
1212
let(:uri) { 'foobar' }
13+
let(:plugindirs) { [] }
1314
let(:target) { inventory.get_target(uri) }
1415
let(:inventory) { Bolt::Inventory.empty }
1516
let(:executor) { Bolt::Executor.new }
@@ -20,7 +21,7 @@
2021
end
2122
let(:pdb_client) { Bolt::PuppetDB::Client.new(config) }
2223
let(:modulepath) { [Bolt::PAL::BOLTLIB_PATH, Bolt::PAL::MODULES_PATH] }
23-
let(:applicator) { Bolt::Applicator.new(inventory, executor, modulepath, [], nil, pdb_client, nil, 2, {}) }
24+
let(:applicator) { Bolt::Applicator.new(inventory, executor, modulepath, plugindirs, nil, pdb_client, nil, 2, {}) }
2425
let(:ast) { { 'resources' => [] } }
2526

2627
let(:report) {
@@ -115,6 +116,61 @@
115116

116117
let(:scope) { double('scope') }
117118

119+
context 'without required modules specified (default)' do
120+
before do
121+
allow(Logging).to receive(:logger).and_return(mock_logger)
122+
allow(mock_logger).to receive(:[]).and_return(mock_logger)
123+
allow(mock_logger).to receive(:'level=').with(any_args)
124+
allow(mock_logger).to receive(:debug).with(any_args)
125+
end
126+
127+
let(:mock_logger) { instance_double("Logging.logger") }
128+
let(:plugindirs) { modulepath }
129+
130+
it 'syncs all modules' do
131+
#
132+
# Use a variable here instead of the Rspec let, so we can mock the logger
133+
#
134+
applicator = Bolt::Applicator.new(inventory, executor, modulepath, plugindirs, nil, pdb_client, nil, 2, {})
135+
expect(applicator).to receive(:compile).and_return(ast)
136+
result = Bolt::Result.new(target, value: report)
137+
allow_any_instance_of(Bolt::Transport::SSH).to receive(:batch_task).and_return(result)
138+
allow(Bolt::ApplyResult).to receive(:puppet_missing_error).with(result).and_return(nil)
139+
140+
expect(mock_logger).to receive(:debug).with(/Packing plugin/).at_least(:once)
141+
expect(mock_logger).to_not receive(:debug).with(/Syncing only required modules/)
142+
applicator.apply([target], :body, scope)
143+
end
144+
end
145+
146+
context 'required modules specified' do
147+
before do
148+
allow(Logging).to receive(:logger).and_return(mock_logger)
149+
allow(mock_logger).to receive(:[]).and_return(mock_logger)
150+
allow(mock_logger).to receive(:'level=').with(any_args)
151+
allow(mock_logger).to receive(:debug).with(any_args)
152+
end
153+
154+
let(:mock_logger) { instance_double("Logging.logger") }
155+
let(:plugindirs) { modulepath }
156+
157+
it 'syncs only required modules' do
158+
#
159+
# Use a variable here instead of the Rspec let, so we can mock the logger
160+
#
161+
applicator = Bolt::Applicator.new(inventory, executor, modulepath, plugindirs, nil, pdb_client, nil, 2, {})
162+
expect(applicator).to receive(:compile).and_return(ast)
163+
result = Bolt::Result.new(target, value: report)
164+
allow_any_instance_of(Bolt::Transport::SSH).to receive(:batch_task).and_return(result)
165+
allow(Bolt::ApplyResult).to receive(:puppet_missing_error).with(result).and_return(nil)
166+
167+
expect(mock_logger).to_not receive(:debug).with(/Packing plugin/)
168+
expect(mock_logger).to receive(:debug).with('Syncing only required modules: just_a_module_name.')
169+
170+
applicator.apply_ast(:body, [target], { required_modules: ['just_a_module_name'] })
171+
end
172+
end
173+
118174
it 'replaces failures to find Puppet' do
119175
expect(applicator).to receive(:compile).and_return(ast)
120176
result = Bolt::Result.new(target, value: report)

0 commit comments

Comments
 (0)