kitchen-dsc 0.12.0 → 0.13.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.
data/Rakefile CHANGED
@@ -1,21 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
- require "chefstyle"
4
+
5
+ require "cookstyle/chefstyle"
3
6
  require "rubocop/rake_task"
4
7
 
5
8
  RuboCop::RakeTask.new(:style) do |task|
6
9
  task.options += ["--display-cop-names", "--no-color"]
7
10
  end
8
11
 
9
- # Create the spec task.
10
12
  require "rspec/core/rake_task"
13
+
14
+ # Named `test` because the shared test-kitchen CI workflow invokes
15
+ # `bundle exec rake test`. Renaming it would break CI.
11
16
  RSpec::Core::RakeTask.new(:test, :tag) do |t, args|
12
17
  t.rspec_opts = [].tap do |a|
13
- a << "--color"
14
18
  a << "--format #{ENV["CI"] ? "documentation" : "progress"}"
15
19
  a << "--backtrace" if ENV["VERBOSE"] || ENV["DEBUG"]
16
20
  a << "--seed #{ENV["SEED"]}" if ENV["SEED"]
17
21
  a << "--tag #{args[:tag]}" if args[:tag]
18
- a << "--default-path test"
19
- a << "-I test/spec"
22
+ a << "--only-failures" if ENV["ONLY_FAILURES"]
20
23
  end.join(" ")
21
24
  end
25
+
26
+ # YARD lives in the :development bundle group, which a test-only install may
27
+ # skip. Documentation tasks are optional, so degrade rather than break `rake`.
28
+ begin
29
+ require "yard"
30
+
31
+ YARD::Rake::YardocTask.new(:yard) do |t|
32
+ t.stats_options = ["--list-undoc"]
33
+ end
34
+
35
+ namespace :yard do
36
+ desc "Report documentation coverage, listing undocumented objects"
37
+ task :stats do
38
+ sh "yard stats --list-undoc"
39
+ end
40
+
41
+ desc "Serve the docs at http://localhost:8808, reloading on change"
42
+ task :server do
43
+ sh "yard server --reload"
44
+ end
45
+ end
46
+ rescue LoadError
47
+ %w{yard yard:stats yard:server}.each do |name|
48
+ desc "(unavailable: install the :development bundle group)" if name == "yard"
49
+ task name do
50
+ abort "YARD is not available. Run `bundle install --with development`."
51
+ end
52
+ end
53
+ end
54
+
55
+ desc "Run the linter and the unit tests"
56
+ task default: %i{style test}
data/kitchen-dsc.gemspec CHANGED
@@ -3,15 +3,15 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require "kitchen-dsc/version"
4
4
 
5
5
  Gem::Specification.new do |gem|
6
- gem.name = "kitchen-dsc"
6
+ gem.name = "kitchen-dsc"
7
+ gem.required_ruby_version = ">= 3.1"
7
8
  gem.version = Kitchen::Dsc::VERSION
8
9
  gem.authors = ["Test Kitchen Team"]
9
10
  gem.email = ["help@sous-chefs.org"]
10
11
  gem.homepage = "https://github.com/test-kitchen/kitchen-dsc"
11
12
  gem.summary = "PowerShell DSC provisioner for test-kitchen"
12
13
  gem.description = "PowerShell DSC provisioner for test-kitchen"
13
- gem.files = `git ls-files`.split($/)
14
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.files = `git ls-files`.split($/)
15
15
  gem.platform = Gem::Platform::RUBY
16
16
  gem.require_paths = ["lib"]
17
17
  gem.license = "Apache-2.0"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Author:: Steven Murawski (<steven.murawski@gmail.com>)
3
5
  #
@@ -13,10 +15,41 @@ require "kitchen/util"
13
15
  require "dsc_lcm_configuration"
14
16
 
15
17
  module Kitchen
18
+ # Test Kitchen's provisioner namespace, reopened to register the DSC
19
+ # provisioner alongside the ones that ship with Test Kitchen itself.
16
20
  module Provisioner
21
+ # Applies PowerShell Desired State Configuration to a Test Kitchen instance.
22
+ #
23
+ # The provisioner runs across the four Test Kitchen phases:
24
+ #
25
+ # 1. {#install_command} configures the Local Configuration Manager on the
26
+ # system under test.
27
+ # 2. {#init_command} creates the remote configuration directory and, on
28
+ # WMF 5, installs any modules requested from a PowerShell gallery.
29
+ # 3. {#create_sandbox} stages DSC resources and the configuration script on
30
+ # the workstation, ready for upload.
31
+ # 4. {#prepare_command} compiles the configuration into a MOF on the system
32
+ # under test and {#run_command} applies it.
33
+ #
34
+ # Two project layouts are supported, chosen automatically by
35
+ # {#powershell_module?}: *module style*, where the kitchen root is itself a
36
+ # PowerShell module (identified by a `<module name>.psd1` manifest), and
37
+ # *repository style*, where DSC resources live in a `modules` directory.
38
+ #
39
+ # @example Minimal kitchen.yml
40
+ # provisioner:
41
+ # name: dsc
42
+ # dsc_local_configuration_manager_version: wmf5
43
+ # configuration_script: web.ps1
44
+ #
45
+ # @see https://github.com/test-kitchen/kitchen-dsc kitchen-dsc README
17
46
  class Dsc < Base
18
47
  kitchen_provisioner_api_version 2
19
48
 
49
+ # @!attribute [rw] tmp_dir
50
+ # @return [String, nil] path to a scratch directory on the system under
51
+ # test. Set by consumers that need somewhere to write intermediate
52
+ # files; unused by the provisioner itself.
20
53
  attr_accessor :tmp_dir
21
54
 
22
55
  default_config :modules_path, "modules"
@@ -37,11 +70,29 @@ module Kitchen
37
70
  default_config :dsc_local_configuration_manager_version, "wmf4"
38
71
  default_config :dsc_local_configuration_manager, {}
39
72
 
73
+ # Resolves the Local Configuration Manager settings before the instance is
74
+ # used.
75
+ #
76
+ # Replaces the caller's partial `:dsc_local_configuration_manager` hash
77
+ # with the fully defaulted settings for the configured WMF version, so
78
+ # later phases and `kitchen diagnose` see the values that will actually be
79
+ # applied.
80
+ #
81
+ # @param instance [Kitchen::Instance] the instance this provisioner serves
82
+ # @return [self]
40
83
  def finalize_config!(instance)
41
84
  config[:dsc_local_configuration_manager] = lcm.lcm_config
42
85
  super(instance)
43
86
  end
44
87
 
88
+ # Builds the command that configures the Local Configuration Manager.
89
+ #
90
+ # Runs during Test Kitchen's `install` phase, before any configuration is
91
+ # compiled, since the LCM controls how DSC behaves for the rest of the
92
+ # run.
93
+ #
94
+ # @return [String] PowerShell that declares and applies the `SetupLCM`
95
+ # meta-configuration
45
96
  def install_command
46
97
  full_lcm_configuration_script = <<-EOH
47
98
  #{lcm.lcm_configuration_script}
@@ -53,6 +104,14 @@ module Kitchen
53
104
  wrap_powershell_code(full_lcm_configuration_script)
54
105
  end
55
106
 
107
+ # Builds the command that prepares the system under test for upload.
108
+ #
109
+ # Always creates the directory the configuration script will be copied
110
+ # into. On WMF 5 with `:modules_from_gallery` set, it also bootstraps
111
+ # PackageManagement and installs those modules.
112
+ #
113
+ # @return [String] PowerShell run during the `converge` phase, before
114
+ # files are transferred
56
115
  def init_command
57
116
  script = <<~EOH
58
117
  #{setup_config_directory_script}
@@ -61,6 +120,17 @@ module Kitchen
61
120
  wrap_powershell_code(script)
62
121
  end
63
122
 
123
+ # Stages DSC resources and the configuration script into the sandbox.
124
+ #
125
+ # The sandbox is the local directory Test Kitchen uploads to the system
126
+ # under test. Which staging strategy runs depends on whether the project
127
+ # is laid out as a PowerShell module or as a repository of modules.
128
+ #
129
+ # @return [void]
130
+ # @raise [Errno::ENOENT] if the configuration script named by
131
+ # `:configuration_script_folder` and `:configuration_script` is missing
132
+ # @see #prepare_resource_style_directory
133
+ # @see #prepare_repo_style_directory
64
134
  def create_sandbox
65
135
  super
66
136
  info("Staging DSC Resource Modules for copy to the SUT")
@@ -73,9 +143,19 @@ module Kitchen
73
143
  prepare_configuration_script
74
144
  end
75
145
 
146
+ # Builds the command that compiles configurations into MOF documents.
147
+ #
148
+ # Copies the uploaded modules onto the `PSModulePath`, loads the
149
+ # configuration script, then compiles each name in `:configuration_name`
150
+ # into `c:/configurations/<name>`. Any leftover MOF from a previous
151
+ # converge is removed first so a failed compile cannot be silently applied.
152
+ #
153
+ # @return [String] PowerShell run after files are transferred and before
154
+ # {#run_command}
76
155
  def prepare_command
77
156
  info("Moving DSC Resources onto PSModulePath")
78
- scripts = <<-EOH
157
+ # +@ makes an explicitly mutable buffer under frozen_string_literal.
158
+ scripts = +<<-EOH
79
159
 
80
160
  if (Test-Path (join-path #{config[:root_path]} 'modules'))
81
161
  {
@@ -133,10 +213,20 @@ module Kitchen
133
213
  wrap_powershell_code(scripts)
134
214
  end
135
215
 
216
+ # Builds the command that applies the compiled MOF documents.
217
+ #
218
+ # A DSC resource may require a reboot to finish. Rather than failing, the
219
+ # generated script reboots the node and exits 35, and this method opts the
220
+ # instance into retrying that exit code so the converge resumes once the
221
+ # node is back. Explicit `:retry_on_exit_code` and `:max_retries` settings
222
+ # are left untouched.
223
+ #
224
+ # @return [String] PowerShell that starts a DSC configuration job per
225
+ # configuration name and reports its errors
136
226
  def run_command
137
227
  config[:retry_on_exit_code] = [35] if config[:retry_on_exit_code].empty?
138
228
  config[:max_retries] = 3 if config[:max_retries] == 1
139
- scripts = ""
229
+ scripts = +""
140
230
  ensure_array(config[:configuration_name]).each do |configuration|
141
231
  info("Running the configuration #{configuration}")
142
232
  run_configuration_script = <<-EOH
@@ -163,6 +253,15 @@ module Kitchen
163
253
 
164
254
  private
165
255
 
256
+ # The Local Configuration Manager configuration for the target WMF version.
257
+ #
258
+ # Note that `DscLcmConfiguration::Factory` only recognizes `"4"`,
259
+ # `"wmf4_with_update"`, `"5"` and `"wmf5"`; every other value, including
260
+ # this provisioner's own `"wmf4"` default, yields the base LCM
261
+ # configuration.
262
+ #
263
+ # @api private
264
+ # @return [DscLcmConfiguration::LcmBase] a memoized LCM configuration
166
265
  def lcm
167
266
  @lcm ||= begin
168
267
  lcm_version = config[:dsc_local_configuration_manager_version]
@@ -171,10 +270,26 @@ module Kitchen
171
270
  end
172
271
  end
173
272
 
273
+ # PowerShell that creates the remote directory holding the configuration
274
+ # script.
275
+ #
276
+ # @api private
277
+ # @return [String] a `mkdir` invocation
174
278
  def setup_config_directory_script
175
279
  "mkdir (split-path (join-path #{config[:root_path]} #{sandboxed_configuration_script})) -force | out-null"
176
280
  end
177
281
 
282
+ # Renders a module specification hash as `install-module` parameters.
283
+ #
284
+ # A `Force` key is dropped because `-force` is already appended to every
285
+ # `install-module` call, and PowerShell rejects a duplicated parameter. A
286
+ # `Repository` key is added from the configured gallery unless the caller
287
+ # supplied one.
288
+ #
289
+ # @api private
290
+ # @param module_specification_hash [Hash] `install-module` parameters, as
291
+ # given in kitchen.yml
292
+ # @return [String] space-separated `-Key Value` pairs
178
293
  def powershell_module_params(module_specification_hash)
179
294
  keys = module_specification_hash.keys.reject { |k| k.to_s.casecmp("force") == 0 }
180
295
  unless keys.any? { |k| k.to_s.downcase == "repository" }
@@ -184,6 +299,13 @@ module Kitchen
184
299
  keys.map { |key| "-#{key} #{module_specification_hash[key]}" }.join(" ")
185
300
  end
186
301
 
302
+ # Builds one `install-module` line per entry in `:modules_from_gallery`.
303
+ #
304
+ # Entries may be plain module names or hashes of `install-module`
305
+ # parameters.
306
+ #
307
+ # @api private
308
+ # @return [Array<String>] PowerShell `install-module` invocations
187
309
  def powershell_modules
188
310
  Array(config[:modules_from_gallery]).map do |powershell_module|
189
311
  params = if powershell_module.is_a? Hash
@@ -195,6 +317,14 @@ module Kitchen
195
317
  end
196
318
  end
197
319
 
320
+ # PowerShell that bootstraps the NuGet package provider.
321
+ #
322
+ # PackageManagement cannot install from a gallery until the NuGet provider
323
+ # is present, and its interactive bootstrap prompt would hang a converge.
324
+ #
325
+ # @api private
326
+ # @return [String, nil] the bootstrap command, or nil when
327
+ # `:nuget_force_bootstrap` is disabled
198
328
  def nuget_force_bootstrap
199
329
  return unless config[:nuget_force_bootstrap]
200
330
 
@@ -202,6 +332,12 @@ module Kitchen
202
332
  "install-packageprovider nuget -force -forcebootstrap | out-null"
203
333
  end
204
334
 
335
+ # The PowerShellGet repository name to install modules from.
336
+ #
337
+ # @api private
338
+ # @return [String] `:gallery_name` when set, the public `PSGallery` when
339
+ # neither gallery setting is given, and `testing` for an unnamed private
340
+ # `:gallery_uri`
205
341
  def psmodule_repository_name
206
342
  return "PSGallery" if config[:gallery_name].nil? && config[:gallery_uri].nil?
207
343
  return "testing" if config[:gallery_name].nil?
@@ -209,6 +345,11 @@ module Kitchen
209
345
  config[:gallery_name]
210
346
  end
211
347
 
348
+ # PowerShell that registers a private gallery as a package source.
349
+ #
350
+ # @api private
351
+ # @return [String, nil] the `register-packagesource` command, or nil when
352
+ # no `:gallery_uri` is configured
212
353
  def register_psmodule_repository
213
354
  return if config[:gallery_uri].nil?
214
355
 
@@ -216,6 +357,11 @@ module Kitchen
216
357
  "register-packagesource -providername PowerShellGet -name '#{psmodule_repository_name}' -location '#{config[:gallery_uri]}' -force -trusted"
217
358
  end
218
359
 
360
+ # PowerShell that installs every requested gallery module.
361
+ #
362
+ # @api private
363
+ # @return [String, nil] the bootstrap, registration and install commands,
364
+ # or nil when no gallery modules are configured
219
365
  def install_module_script
220
366
  return if config[:modules_from_gallery].nil?
221
367
 
@@ -226,28 +372,67 @@ module Kitchen
226
372
  EOH
227
373
  end
228
374
 
375
+ # Whether gallery modules should be installed during {#init_command}.
376
+ #
377
+ # Gallery installation depends on PowerShellGet, which ships with WMF 5.
378
+ #
379
+ # @api private
380
+ # @return [Boolean] true only when targeting WMF 5 with modules requested
229
381
  def install_modules?
230
382
  config[:dsc_local_configuration_manager_version] == "wmf5" &&
231
383
  !config[:modules_from_gallery].nil?
232
384
  end
233
385
 
386
+ # Name of the PowerShell variable holding configuration data.
387
+ #
388
+ # @api private
389
+ # @return [String] `:configuration_data_variable`, or `ConfigurationData`
390
+ # when it was explicitly blanked out
234
391
  def configuration_data_variable
235
392
  config[:configuration_data_variable].nil? ? "ConfigurationData" : config[:configuration_data_variable]
236
393
  end
237
394
 
395
+ # PowerShell that assigns `:configuration_data` to its variable.
396
+ #
397
+ # @api private
398
+ # @return [String] a hashtable assignment
238
399
  def configuration_data_assignment
239
400
  "$" + configuration_data_variable + " = " + ps_hash(config[:configuration_data])
240
401
  end
241
402
 
403
+ # Wraps generated PowerShell for execution by the transport.
404
+ #
405
+ # Progress streams are silenced first: WinRM relays them as output, which
406
+ # makes converge logs unreadable and can slow long-running resources.
407
+ #
408
+ # @api private
409
+ # @param code [String] the PowerShell to wrap
410
+ # @return [String] the wrapped command
242
411
  def wrap_powershell_code(code)
243
412
  wrap_shell_code(["$ProgressPreference = 'SilentlyContinue';", code].join("\n"))
244
413
  end
245
414
 
415
+ # Whether the kitchen root is itself a PowerShell module.
416
+ #
417
+ # Detected by a `<module name>.psd1` manifest sitting beside the project,
418
+ # which selects module-style staging over repository-style staging.
419
+ #
420
+ # @api private
421
+ # @return [Boolean] true when a matching module manifest exists
246
422
  def powershell_module?
247
423
  module_metadata_file = File.join(config[:kitchen_root], "#{module_name}.psd1")
248
424
  File.exist?(module_metadata_file)
249
425
  end
250
426
 
427
+ # Lists the files to stage from a module-style project.
428
+ #
429
+ # Directories are excluded because the copy recreates them as needed, and
430
+ # repository housekeeping files are excluded because they are not part of
431
+ # the module.
432
+ #
433
+ # @api private
434
+ # @param path [String] directory to enumerate
435
+ # @return [Array<String>] absolute paths of files to stage
251
436
  def list_files(path)
252
437
  base_directory_content = Dir.glob(File.join(path, "*"))
253
438
  nested_directory_content = Dir.glob(File.join(path, "*/**/*"))
@@ -260,10 +445,22 @@ module Kitchen
260
445
  end
261
446
  end
262
447
 
448
+ # The PowerShell module name implied by the project directory.
449
+ #
450
+ # @api private
451
+ # @return [String] basename of `:kitchen_root`
263
452
  def module_name
264
453
  File.basename(config[:kitchen_root])
265
454
  end
266
455
 
456
+ # Stages a module-style project into the sandbox.
457
+ #
458
+ # The whole kitchen root is copied to `modules/<module name>` so the
459
+ # module lands on the system under test's `PSModulePath` under the name
460
+ # DSC expects.
461
+ #
462
+ # @api private
463
+ # @return [void]
267
464
  def prepare_resource_style_directory
268
465
  sandbox_base_module_path = File.join(sandbox_path, "modules/#{module_name}")
269
466
 
@@ -277,6 +474,13 @@ module Kitchen
277
474
  end
278
475
  end
279
476
 
477
+ # Stages a repository-style project into the sandbox.
478
+ #
479
+ # A missing modules directory is not an error: a project may ship only a
480
+ # configuration script and rely on resources already present on the node.
481
+ #
482
+ # @api private
483
+ # @return [void]
280
484
  def prepare_repo_style_directory
281
485
  module_path = File.join(config[:kitchen_root], config[:modules_path])
282
486
  sandbox_module_path = File.join(sandbox_path, "modules")
@@ -289,14 +493,38 @@ module Kitchen
289
493
  end
290
494
  end
291
495
 
496
+ # Path of the configuration script relative to the sandbox and to
497
+ # `:root_path` on the system under test.
498
+ #
499
+ # @api private
500
+ # @return [String] the sandboxed script path
292
501
  def sandboxed_configuration_script
293
502
  File.join("configuration", config[:configuration_script])
294
503
  end
295
504
 
505
+ # Indentation used when rendering PowerShell hashtables.
506
+ #
507
+ # @api private
508
+ # @param depth [Integer] number of spaces
509
+ # @return [String] a run of spaces
296
510
  def pad(depth = 0)
297
511
  " " * depth
298
512
  end
299
513
 
514
+ # Renders a Ruby object as a PowerShell literal.
515
+ #
516
+ # Hashes become hashtables and arrays become arrays; every other value is
517
+ # rendered as a double-quoted string, so Ruby booleans and integers reach
518
+ # DSC quoted.
519
+ #
520
+ # @api private
521
+ # @param obj [Hash, Array, Object] the value to render
522
+ # @param depth [Integer] current indentation depth
523
+ # @return [String] a PowerShell literal
524
+ #
525
+ # @example
526
+ # ps_hash("AllNodes" => [{ "NodeName" => "*" }])
527
+ # #=> %{@{\n "AllNodes" = @(\n@{\n "NodeName" = "*"\n }\n)\n}}
300
528
  def ps_hash(obj, depth = 0)
301
529
  if obj.is_a?(Hash)
302
530
  obj.map do |k, v|
@@ -310,6 +538,11 @@ module Kitchen
310
538
  end
311
539
  end
312
540
 
541
+ # Copies the DSC configuration script into the sandbox.
542
+ #
543
+ # @api private
544
+ # @return [void]
545
+ # @raise [Errno::ENOENT] if the configured script does not exist
313
546
  def prepare_configuration_script
314
547
  configuration_script_file = File.join(config[:configuration_script_folder], config[:configuration_script])
315
548
  configuration_script_path = File.join(config[:kitchen_root], configuration_script_file)
@@ -319,6 +552,12 @@ module Kitchen
319
552
  FileUtils.cp(configuration_script_path, sandbox_configuration_script_path)
320
553
  end
321
554
 
555
+ # Wraps a scalar in an array so `:configuration_name` may be given either
556
+ # as a single name or as a list.
557
+ #
558
+ # @api private
559
+ # @param thing [Object, Array] the value to normalize
560
+ # @return [Array] +thing+ if it is already an array, otherwise `[thing]`
322
561
  def ensure_array(thing)
323
562
  if thing.is_a?(Array)
324
563
  thing
@@ -1,5 +1,23 @@
1
+ #
2
+ # Author:: Steven Murawski (<steven.murawski@gmail.com>)
3
+ #
4
+ # Copyright (C) 2014 Steven Murawski
5
+ #
6
+ # Licensed under the Apache 2 License.
7
+ # See LICENSE for more details
8
+
1
9
  module Kitchen
10
+ # Namespace for the kitchen-dsc plugin's own metadata.
11
+ #
12
+ # The provisioner itself lives in {Kitchen::Provisioner::Dsc}; this module
13
+ # exists so the gemspec can read the version without loading Test Kitchen.
2
14
  module Dsc
3
- VERSION = "0.12.0".freeze
15
+ # The released version of the kitchen-dsc gem.
16
+ #
17
+ # Kept in sync with the gemspec by release-please; changing it by hand is
18
+ # only appropriate as part of a release commit.
19
+ #
20
+ # @return [String] a frozen dotted version number
21
+ VERSION = "0.13.0".freeze
4
22
  end
5
23
  end
@@ -0,0 +1,12 @@
1
+ {
2
+ "packages": {
3
+ ".": {
4
+ "package-name": "kitchen-dsc",
5
+ "changelog-path": "CHANGELOG.md",
6
+ "release-type": "ruby",
7
+ "include-component-in-tag": false,
8
+ "version-file": "lib/kitchen-dsc/version.rb"
9
+ }
10
+ },
11
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
12
+ }