test-kitchen 3.9.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,723 +0,0 @@
1
- #
2
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
- #
4
- # Copyright (C) 2013, Fletcher Nichol
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # https://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
-
18
- require "fileutils" unless defined?(FileUtils)
19
- require "pathname" unless defined?(Pathname)
20
- require "json" unless defined?(JSON)
21
- require "cgi" unless defined?(CGI)
22
-
23
- require_relative "chef/policyfile"
24
- require_relative "chef/berkshelf"
25
- require_relative "chef/common_sandbox"
26
- require_relative "../util"
27
- module LicenseAcceptance
28
- autoload :Acceptor, "license_acceptance/acceptor"
29
- end
30
-
31
- begin
32
- require "chef-config/config"
33
- require "chef-config/workstation_config_loader"
34
- rescue LoadError # rubocop:disable Lint/HandleExceptions
35
- # This space left intentionally blank.
36
- end
37
-
38
- module Kitchen
39
- module Provisioner
40
- # Common implementation details for Chef-related provisioners.
41
- #
42
- # @author Fletcher Nichol <fnichol@nichol.ca>
43
- class ChefBase < Base
44
- default_config :require_chef_omnibus, true
45
- default_config :chef_omnibus_url, "https://omnitruck.chef.io/install.sh"
46
- default_config :chef_omnibus_install_options, nil
47
- default_config :chef_license, nil
48
- default_config :run_list, []
49
- default_config :policy_group, nil
50
- default_config :attributes, {}
51
- default_config :config_path, nil
52
- default_config :log_file, nil
53
- default_config :log_level do |provisioner|
54
- provisioner[:debug] ? "debug" : "auto"
55
- end
56
- default_config :profile_ruby, false
57
- # The older policyfile_zero used `policyfile` so support it for compat.
58
- default_config :policyfile, nil
59
- # Will try to autodetect by searching for `Policyfile.rb` if not set.
60
- # If set, will error if the file doesn't exist.
61
- default_config :policyfile_path, nil
62
- # Will try to autodetect by searching for `Berksfile` if not set.
63
- # If set, will error if the file doesn't exist.
64
- default_config :berksfile_path, nil
65
- # If set to true (which is the default from `chef generate`), try to update
66
- # backend cookbook downloader on every kitchen run.
67
- default_config :always_update_cookbooks, true
68
- default_config :cookbook_files_glob, %w(
69
- README.* VERSION metadata.{json,rb} attributes.rb recipe.rb
70
- attributes/**/* definitions/**/* files/**/* libraries/**/*
71
- providers/**/* recipes/**/* resources/**/* templates/**/*
72
- ohai/**/* compliance/**/*
73
- ).join(",")
74
- # to ease upgrades, allow the user to turn deprecation warnings into errors
75
- default_config :deprecations_as_errors, false
76
-
77
- # Override the default from Base so reboot handling works by default for Chef.
78
- default_config :retry_on_exit_code, [35, 213]
79
-
80
- default_config :multiple_converge, 1
81
-
82
- default_config :enforce_idempotency, false
83
-
84
- default_config :data_path do |provisioner|
85
- provisioner.calculate_path("data")
86
- end
87
- expand_path_for :data_path
88
-
89
- default_config :data_bags_path do |provisioner|
90
- provisioner.calculate_path("data_bags")
91
- end
92
- expand_path_for :data_bags_path
93
-
94
- default_config :environments_path do |provisioner|
95
- provisioner.calculate_path("environments")
96
- end
97
- expand_path_for :environments_path
98
-
99
- default_config :nodes_path do |provisioner|
100
- provisioner.calculate_path("nodes")
101
- end
102
- expand_path_for :nodes_path
103
-
104
- default_config :roles_path do |provisioner|
105
- provisioner.calculate_path("roles")
106
- end
107
- expand_path_for :roles_path
108
-
109
- default_config :clients_path do |provisioner|
110
- provisioner.calculate_path("clients")
111
- end
112
- expand_path_for :clients_path
113
-
114
- default_config :encrypted_data_bag_secret_key_path do |provisioner|
115
- provisioner.calculate_path("encrypted_data_bag_secret_key", type: :file)
116
- end
117
- expand_path_for :encrypted_data_bag_secret_key_path
118
-
119
- #
120
- # New configuration options per RFC 091
121
- # https://github.com/chef/chef-rfc/blob/master/rfc091-deprecate-kitchen-settings.md
122
- #
123
-
124
- # Setting product_name to nil. It is currently the pivot point
125
- # between the two install paths (Mixlib::Install::ScriptGenerator and Mixlib::Install)
126
- default_config :product_name
127
-
128
- default_config :product_version, :latest
129
-
130
- default_config :channel, :stable
131
-
132
- default_config :install_strategy, "once"
133
-
134
- default_config :platform
135
-
136
- default_config :platform_version
137
-
138
- default_config :architecture
139
-
140
- default_config :download_url
141
-
142
- default_config :checksum
143
-
144
- deprecate_config_for :require_chef_omnibus do |provisioner|
145
- case
146
- when provisioner[:require_chef_omnibus] == false
147
- Util.outdent!(<<-MSG)
148
- The 'require_chef_omnibus' attribute with value of 'false' will
149
- change to use the new 'install_strategy' attribute with a value of 'skip'.
150
-
151
- Note: 'product_name' must be set in order to use 'install_strategy'.
152
- Although this seems counterintuitive, it is necessary until
153
- 'product_name' replaces 'require_chef_omnibus' as the default.
154
-
155
- # New Usage #
156
- provisioner:
157
- product_name: <chef or chef-workstation>
158
- install_strategy: skip
159
- MSG
160
- when provisioner[:require_chef_omnibus].to_s.match?(/\d/)
161
- Util.outdent!(<<-MSG)
162
- The 'require_chef_omnibus' attribute with version values will change
163
- to use the new 'product_version' attribute.
164
-
165
- Note: 'product_name' must be set in order to use 'product_version'
166
- until 'product_name' replaces 'require_chef_omnibus' as the default.
167
-
168
- # New Usage #
169
- provisioner:
170
- product_name: <chef or chef-workstation>
171
- product_version: #{provisioner[:require_chef_omnibus]}
172
- MSG
173
- when provisioner[:require_chef_omnibus] == "latest"
174
- Util.outdent!(<<-MSG)
175
- The 'require_chef_omnibus' attribute with value of 'latest' will change
176
- to use the new 'install_strategy' attribute with a value of 'always'.
177
-
178
- Note: 'product_name' must be set in order to use 'install_strategy'
179
- until 'product_name' replaces 'require_chef_omnibus' as the default.
180
-
181
- # New Usage #
182
- provisioner:
183
- product_name: <chef or chef-workstation>
184
- install_strategy: always
185
- MSG
186
- end
187
- end
188
-
189
- deprecate_config_for :chef_omnibus_url, Util.outdent!(<<-MSG)
190
- Changing the 'chef_omnibus_url' attribute breaks existing functionality. It will
191
- be removed in a future version.
192
- MSG
193
-
194
- deprecate_config_for :chef_omnibus_install_options, Util.outdent!(<<-MSG)
195
- The 'chef_omnibus_install_options' attribute will be replaced by using
196
- 'product_name' and 'channel' attributes.
197
-
198
- Note: 'product_name' must be set in order to use 'channel'
199
- until 'product_name' replaces 'require_chef_omnibus' as the default.
200
-
201
- # Deprecated Example #
202
- provisioner:
203
- chef_omnibus_install_options: -P chef-workstation -c current
204
-
205
- # New Usage #
206
- provisioner:
207
- product_name: chef-workstation
208
- channel: current
209
- MSG
210
-
211
- deprecate_config_for :install_msi_url, Util.outdent!(<<-MSG)
212
- The 'install_msi_url' will be relaced by the 'download_url' attribute.
213
- 'download_url' will be applied to Bourne and PowerShell download scripts.
214
-
215
- Note: 'product_name' must be set in order to use 'download_url'
216
- until 'product_name' replaces 'require_chef_omnibus' as the default.
217
-
218
- # New Usage #
219
- provisioner:
220
- product_name: <chef or chef-workstation>
221
- download_url: http://direct-download-url
222
- MSG
223
-
224
- deprecate_config_for :chef_metadata_url, Util.outdent!(<<-MSG)
225
- The 'chef_metadata_url' will be removed. The Windows metadata URL will be
226
- fully managed by using attribute settings.
227
- MSG
228
-
229
- # Reads the local Chef::Config object (if present). We do this because
230
- # we want to start bring Chef config and Chef Workstation config closer
231
- # together. For example, we want to configure proxy settings in 1
232
- # location instead of 3 configuration files.
233
- #
234
- # @param config [Hash] initial provided configuration
235
- def initialize(config = {})
236
- super(config)
237
-
238
- if defined?(ChefConfig::WorkstationConfigLoader)
239
- ChefConfig::WorkstationConfigLoader.new(config[:config_path]).load
240
- end
241
- # This exports any proxy config present in the Chef config to
242
- # appropriate environment variables, which Test Kitchen respects
243
- ChefConfig::Config.export_proxies if defined?(ChefConfig::Config.export_proxies)
244
- end
245
-
246
- def doctor(state)
247
- deprecated_config = instance.driver.instance_variable_get(:@deprecated_config)
248
- deprecated_config.each do |attr, msg|
249
- info("**** #{attr} deprecated\n#{msg}")
250
- end
251
- end
252
-
253
- # gives us the product version from either require_chef_omnibus or product_version
254
- # If the non-default (true) value of require_chef_omnibus is present use that
255
- # otherwise use config[:product_version] which defaults to :latest and is the actual
256
- # default for chef provisioners
257
- #
258
- # @return [String,Symbol,NilClass] version or nil if not applicable
259
- def product_version
260
- case config[:require_chef_omnibus]
261
- when FalseClass
262
- nil
263
- when TrueClass
264
- config[:product_version]
265
- else
266
- config[:require_chef_omnibus]
267
- end
268
- end
269
-
270
- # If the user has policyfiles we shell out to the `chef` executable, so need to ensure they have
271
- # accepted the Chef Workstation license. Otherwise they just need the Chef Infra license.
272
- #
273
- # @return [String] license id to prompt for acceptance
274
- def license_acceptance_id
275
- case
276
- when File.exist?(policyfile) && (config[:product_name].nil? || config[:product_name].start_with?("chef"))
277
- "chef-workstation"
278
- when config[:product_name]
279
- config[:product_name]
280
- else
281
- "chef"
282
- end
283
- end
284
-
285
- # (see Base#check_license)
286
- def check_license
287
- name = license_acceptance_id
288
- version = product_version
289
- debug("Checking if we need to prompt for license acceptance on product: #{name} version: #{version}.")
290
-
291
- acceptor = LicenseAcceptance::Acceptor.new(logger: Kitchen.logger, provided: config[:chef_license])
292
- if acceptor.license_required?(name, version)
293
- debug("License acceptance required for #{name} version: #{version}. Prompting")
294
- license_id = acceptor.id_from_mixlib(name)
295
- begin
296
- acceptor.check_and_persist(license_id, version.to_s)
297
- rescue LicenseAcceptance::LicenseNotAcceptedError => e
298
- error("Cannot converge without accepting the #{e.product.pretty_name} License. Set it in your kitchen.yml or using the CHEF_LICENSE environment variable")
299
- raise
300
- end
301
- config[:chef_license] ||= acceptor.acceptance_value
302
- end
303
- end
304
-
305
- # (see Base#create_sandbox)
306
- def create_sandbox
307
- super
308
- sanity_check_sandbox_options!
309
- Chef::CommonSandbox.new(config, sandbox_path, instance).populate
310
- end
311
-
312
- # (see Base#init_command)
313
- def init_command
314
- dirs = %w{
315
- cookbooks data data_bags environments roles clients
316
- encrypted_data_bag_secret
317
- }.sort.map { |dir| remote_path_join(config[:root_path], dir) }
318
-
319
- vars = if powershell_shell?
320
- init_command_vars_for_powershell(dirs)
321
- else
322
- init_command_vars_for_bourne(dirs)
323
- end
324
-
325
- prefix_command(shell_code_from_file(vars, "chef_base_init_command"))
326
- end
327
-
328
- # (see Base#install_command)
329
- def install_command
330
- return unless config[:require_chef_omnibus] || config[:product_name]
331
- return if config[:product_name] && config[:install_strategy] == "skip"
332
-
333
- prefix_command(install_script_contents)
334
- end
335
-
336
- private
337
-
338
- def last_exit_code
339
- "; exit $LastExitCode" if powershell_shell?
340
- end
341
-
342
- # @return [Hash] an option hash for the install commands
343
- # @api private
344
- def install_options
345
- add_omnibus_directory_option if instance.driver.cache_directory
346
- project = /\s*-P (\w+)\s*/.match(config[:chef_omnibus_install_options])
347
- {
348
- omnibus_url: config[:chef_omnibus_url],
349
- project: project.nil? ? nil : project[1],
350
- install_flags: config[:chef_omnibus_install_options],
351
- sudo_command:,
352
- }.tap do |opts|
353
- opts[:root] = config[:chef_omnibus_root] if config.key? :chef_omnibus_root
354
- %i{install_msi_url http_proxy https_proxy}.each do |key|
355
- opts[key] = config[key] if config.key? key
356
- end
357
- end
358
- end
359
-
360
- # Verify if the "omnibus_dir_option" has already been passed, if so we
361
- # don't use the @driver.cache_directory
362
- #
363
- # @api private
364
- def add_omnibus_directory_option
365
- cache_dir_option = "#{omnibus_dir_option} #{instance.driver.cache_directory}"
366
- if config[:chef_omnibus_install_options].nil?
367
- config[:chef_omnibus_install_options] = cache_dir_option
368
- elsif config[:chef_omnibus_install_options].match(/\s*#{omnibus_dir_option}\s*/).nil?
369
- config[:chef_omnibus_install_options] << " " << cache_dir_option
370
- end
371
- end
372
-
373
- # @return [String] an absolute path to a Policyfile, relative to the
374
- # kitchen root
375
- # @api private
376
- def policyfile
377
- policyfile_basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
378
- File.expand_path(policyfile_basename, config[:kitchen_root])
379
- end
380
-
381
- # @return [String] an absolute path to a Berksfile, relative to the
382
- # kitchen root
383
- # @api private
384
- def berksfile
385
- berksfile_basename = config[:berksfile_path] || config[:berksfile] || "Berksfile"
386
- File.expand_path(berksfile_basename, config[:kitchen_root])
387
- end
388
-
389
- # Generates a Hash with default values for a solo.rb or client.rb Chef
390
- # configuration file.
391
- #
392
- # @return [Hash] a configuration hash
393
- # @api private
394
- def default_config_rb # rubocop:disable Metrics/MethodLength
395
- root = config[:root_path].gsub("$env:TEMP", "\#{ENV['TEMP']}")
396
-
397
- config_rb = {
398
- node_name: instance.name,
399
- checksum_path: remote_path_join(root, "checksums"),
400
- file_cache_path: remote_path_join(root, "cache"),
401
- file_backup_path: remote_path_join(root, "backup"),
402
- cookbook_path: [
403
- remote_path_join(root, "cookbooks"),
404
- remote_path_join(root, "site-cookbooks"),
405
- ],
406
- data_bag_path: remote_path_join(root, "data_bags"),
407
- environment_path: remote_path_join(root, "environments"),
408
- node_path: remote_path_join(root, "nodes"),
409
- role_path: remote_path_join(root, "roles"),
410
- client_path: remote_path_join(root, "clients"),
411
- user_path: remote_path_join(root, "users"),
412
- validation_key: remote_path_join(root, "validation.pem"),
413
- client_key: remote_path_join(root, "client.pem"),
414
- chef_server_url: "http://127.0.0.1:8889",
415
- encrypted_data_bag_secret: remote_path_join(
416
- root, "encrypted_data_bag_secret"
417
- ),
418
- treat_deprecation_warnings_as_errors: config[:deprecations_as_errors],
419
- }
420
- config_rb[:chef_license] = config[:chef_license] unless config[:chef_license].nil?
421
- config_rb
422
- end
423
-
424
- # Generates a rendered client.rb/solo.rb/knife.rb formatted file as a
425
- # String.
426
- #
427
- # @param data [Hash] a key/value pair hash of configuration
428
- # @return [String] a rendered Chef config file as a String
429
- # @api private
430
- def format_config_file(data)
431
- data.each.map do |attr, value|
432
- [attr, format_value(value)].join(" ")
433
- end.join("\n")
434
- end
435
-
436
- # Converts a Ruby object to a String interpretation suitable for writing
437
- # out to a client.rb/solo.rb/knife.rb file.
438
- #
439
- # @param obj [Object] an object
440
- # @return [String] a string representation
441
- # @api private
442
- def format_value(obj)
443
- if obj.is_a?(String) && obj =~ /^:/
444
- obj
445
- elsif obj.is_a?(String)
446
- %{"#{obj.gsub("\\", "\\\\\\\\")}"}
447
- elsif obj.is_a?(Array)
448
- %{[#{obj.map { |i| format_value(i) }.join(", ")}]}
449
- else
450
- obj.inspect
451
- end
452
- end
453
-
454
- # Generates the init command variables for Bourne shell-based platforms.
455
- #
456
- # @param dirs [Array<String>] directories
457
- # @return [String] shell variable lines
458
- # @api private
459
- def init_command_vars_for_bourne(dirs)
460
- [
461
- shell_var("sudo_rm", sudo("rm")),
462
- shell_var("dirs", dirs.join(" ")),
463
- shell_var("root_path", config[:root_path]),
464
- ].join("\n")
465
- end
466
-
467
- # Generates the init command variables for PowerShell-based platforms.
468
- #
469
- # @param dirs [Array<String>] directories
470
- # @return [String] shell variable lines
471
- # @api private
472
- def init_command_vars_for_powershell(dirs)
473
- [
474
- %{$dirs = @(#{dirs.map { |d| %{"#{d}"} }.join(", ")})},
475
- shell_var("root_path", config[:root_path]),
476
- ].join("\n")
477
- end
478
-
479
- # Load cookbook dependency resolver code, if required.
480
- #
481
- # (see Base#load_needed_dependencies!)
482
- def load_needed_dependencies!
483
- super
484
- if File.exist?(policyfile)
485
- debug("Policyfile found at #{policyfile}, using Policyfile to resolve cookbook dependencies")
486
- Chef::Policyfile.load!(logger:)
487
- elsif File.exist?(berksfile)
488
- debug("Berksfile found at #{berksfile}, using Berkshelf to resolve cookbook dependencies")
489
- Chef::Berkshelf.load!(logger:)
490
- end
491
- end
492
-
493
- # @return [String] contents of the install script
494
- # @api private
495
- def install_script_contents
496
- # by default require_chef_omnibus is set to true. Check config[:product_name] first
497
- # so that we can use it if configured.
498
- if config[:product_name]
499
- script_for_product
500
- elsif config[:require_chef_omnibus]
501
- script_for_omnibus_version
502
- end
503
- end
504
-
505
- # @return [String] contents of product based install script
506
- # @api private
507
- def script_for_product
508
- require "mixlib/install"
509
- installer = Mixlib::Install.new({
510
- product_name: config[:product_name],
511
- product_version: config[:product_version],
512
- channel: config[:channel].to_sym,
513
- install_command_options: {
514
- install_strategy: config[:install_strategy],
515
- },
516
- }.tap do |opts|
517
- opts[:shell_type] = :ps1 if powershell_shell?
518
- %i{platform platform_version architecture}.each do |key|
519
- opts[key] = config[key] if config[key]
520
- end
521
-
522
- unless windows_os?
523
- # omnitruck installer does not currently support a tmp dir option on windows
524
- opts[:install_command_options][:tmp_dir] = config[:root_path]
525
- opts[:install_command_options]["TMPDIR"] = config[:root_path]
526
- end
527
-
528
- if config[:download_url]
529
- opts[:install_command_options][:download_url_override] = config[:download_url]
530
- opts[:install_command_options][:checksum] = config[:checksum] if config[:checksum]
531
- end
532
-
533
- if instance.driver.cache_directory
534
- download_dir_option = windows_os? ? :download_directory : :cmdline_dl_dir
535
- opts[:install_command_options][download_dir_option] = instance.driver.cache_directory
536
- end
537
-
538
- proxies = {}.tap do |prox|
539
- %i{http_proxy https_proxy ftp_proxy no_proxy}.each do |key|
540
- prox[key] = config[key] if config[key]
541
- end
542
-
543
- # install.ps1 only supports http_proxy
544
- prox.delete_if { |p| %i{https_proxy ftp_proxy no_proxy}.include?(p) } if powershell_shell?
545
- end
546
- opts[:install_command_options].merge!(proxies)
547
- end)
548
- config[:chef_omnibus_root] = installer.root
549
- if powershell_shell?
550
- installer.install_command
551
- else
552
- install_from_file(installer.install_command)
553
- end
554
- end
555
-
556
- # @return [String] Correct option per platform to specify the the
557
- # cache directory
558
- # @api private
559
- def omnibus_dir_option
560
- windows_os? ? "-download_directory" : "-d"
561
- end
562
-
563
- def install_from_file(command)
564
- install_file = "#{config[:root_path]}/chef-installer.sh"
565
- script = []
566
- script << "mkdir -p #{config[:root_path]}"
567
- script << "if [ $? -ne 0 ]; then"
568
- script << " echo Kitchen config setting root_path: '#{config[:root_path]}' not creatable by regular user "
569
- script << " exit 1"
570
- script << "fi"
571
- script << "cat > #{install_file} <<\"EOL\""
572
- script << command
573
- script << "EOL"
574
- script << "chmod +x #{install_file}"
575
- script << sudo(install_file)
576
- script.join("\n")
577
- end
578
-
579
- # @return [String] contents of version based install script
580
- # @api private
581
- def script_for_omnibus_version
582
- require "mixlib/install/script_generator"
583
- installer = Mixlib::Install::ScriptGenerator.new(
584
- config[:require_chef_omnibus], powershell_shell?, install_options
585
- )
586
- config[:chef_omnibus_root] = installer.root
587
- sudo(installer.install_command)
588
- end
589
-
590
- # Hook used in subclasses to indicate support for policyfiles.
591
- #
592
- # @abstract
593
- # @return [Boolean]
594
- # @api private
595
- def supports_policyfile?
596
- false
597
- end
598
-
599
- # @return [void]
600
- # @raise [UserError]
601
- # @api private
602
- def sanity_check_sandbox_options!
603
- if (config[:policyfile_path] || config[:policyfile]) && !File.exist?(policyfile)
604
- raise UserError, "policyfile_path set in config " \
605
- "(#{config[:policyfile_path]} could not be found. " \
606
- "Expected to find it at full path #{policyfile}."
607
- end
608
- if config[:berksfile_path] && !File.exist?(berksfile)
609
- raise UserError, "berksfile_path set in config " \
610
- "(#{config[:berksfile_path]} could not be found. " \
611
- "Expected to find it at full path #{berksfile}."
612
- end
613
- if File.exist?(policyfile) && !supports_policyfile?
614
- raise UserError, "policyfile detected, but provisioner " \
615
- "#{self.class.name} doesn't support Policyfiles. " \
616
- "Either use a different provisioner, or delete/rename " \
617
- "#{policyfile}."
618
- end
619
- end
620
-
621
- # Writes a configuration file to the sandbox directory.
622
- # @api private
623
- def prepare_config_rb
624
- data = default_config_rb.merge(config[config_filename.tr(".", "_").to_sym])
625
- data = data.merge(named_run_list: config[:named_run_list]) if config[:named_run_list]
626
-
627
- info("Preparing #{config_filename}")
628
- debug("Creating #{config_filename} from #{data.inspect}")
629
-
630
- File.open(File.join(sandbox_path, config_filename), "wb") do |file|
631
- file.write(format_config_file(data))
632
- end
633
-
634
- prepare_config_idempotency_check(data) if config[:enforce_idempotency]
635
- end
636
-
637
- # Writes a configuration file to the sandbox directory
638
- # to check for idempotency of the run.
639
- # @api private
640
- def prepare_config_idempotency_check(data)
641
- handler_filename = "chef-client-fail-if-update-handler.rb"
642
- source = File.join(
643
- File.dirname(__FILE__), %w{.. .. .. support }, handler_filename
644
- )
645
- FileUtils.cp(source, File.join(sandbox_path, handler_filename))
646
- File.open(File.join(sandbox_path, "client_no_updated_resources.rb"), "wb") do |file|
647
- file.write(format_config_file(data))
648
- file.write("\n\n")
649
- file.write("handler_file = File.join(File.dirname(__FILE__), '#{handler_filename}')\n")
650
- file.write "Chef::Config.from_file(handler_file)\n"
651
- end
652
- end
653
-
654
- # Returns an Array of command line arguments for the chef client.
655
- #
656
- # @return [Array<String>] an array of command line arguments
657
- # @api private
658
- def chef_args(_config_filename)
659
- raise "You must override in sub classes!"
660
- end
661
-
662
- # Returns a filename for the configuration file
663
- # defaults to client.rb
664
- #
665
- # @return [String] a filename
666
- # @api private
667
- def config_filename
668
- "client.rb"
669
- end
670
-
671
- # Gives the command used to run chef
672
- # @api private
673
- def chef_cmd(base_cmd)
674
- if windows_os?
675
- separator = [
676
- "; if ($LastExitCode -ne 0) { ",
677
- "throw \"Command failed with exit code $LastExitCode.\" } ;",
678
- ].join
679
- else
680
- separator = " && "
681
- end
682
- chef_cmds(base_cmd).join(separator)
683
- end
684
-
685
- # Gives an array of commands
686
- # @api private
687
- def chef_cmds(base_cmd)
688
- cmds = []
689
- num_converges = config[:multiple_converge].to_i
690
- idempotency = config[:enforce_idempotency]
691
-
692
- # Execute Chef Client n-1 times, without exiting
693
- (num_converges - 1).times do
694
- cmds << wrapped_chef_cmd(base_cmd, config_filename)
695
- end
696
-
697
- # Append another execution with Windows specific Exit code helper or (for
698
- # idempotency check) a specific config file which assures no changed resources.
699
- cmds << unless idempotency
700
- wrapped_chef_cmd(base_cmd, config_filename, append: last_exit_code)
701
- else
702
- wrapped_chef_cmd(base_cmd, "client_no_updated_resources.rb", append: last_exit_code)
703
- end
704
- cmds
705
- end
706
-
707
- # Concatenate all arguments and wrap it with shell-specifics
708
- # @api private
709
- def wrapped_chef_cmd(base_cmd, configfile, append: "")
710
- args = []
711
-
712
- args << base_cmd
713
- args << chef_args(configfile)
714
- args << append
715
-
716
- shell_cmd = args.flatten.join(" ")
717
- shell_cmd = shell_cmd.prepend(reload_ps1_path) if windows_os?
718
-
719
- prefix_command(wrap_shell_code(shell_cmd))
720
- end
721
- end
722
- end
723
- end