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.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/lint.yml +3 -3
- data/.github/workflows/publish.yaml +2 -5
- data/.gitignore +3 -0
- data/.markdownlint.yaml +2 -5
- data/.release-please-manifest.json +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1 -4
- data/.yamllint +6 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +121 -0
- data/Gemfile +7 -9
- data/README.md +230 -79
- data/Rakefile +40 -5
- data/kitchen-dsc.gemspec +3 -3
- data/lib/kitchen/provisioner/dsc.rb +241 -2
- data/lib/kitchen-dsc/version.rb +19 -1
- data/release-please-config.json +12 -0
- data/spec/kitchen/provisioner/dsc_spec.rb +590 -0
- data/spec/kitchen_dsc/version_spec.rb +21 -0
- data/spec/spec_helper.rb +70 -16
- data/spec/support/kitchen_helpers.rb +159 -0
- metadata +15 -9
- data/lib/kitchen/provisioner/dsc_lcm/lcm_base.rb +0 -86
- data/lib/kitchen/provisioner/dsc_lcm/lcm_v4.rb +0 -52
- data/lib/kitchen/provisioner/dsc_lcm/lcm_v5.rb +0 -53
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Kitchen::Provisioner::Dsc do
|
|
4
|
+
subject(:provisioner) { build_provisioner }
|
|
5
|
+
|
|
6
|
+
# Every public command is wrapped by Kitchen::Configurable#wrap_shell_code,
|
|
7
|
+
# which injects `$env:TEST_KITCHEN` and — only when ENV["CI"] is set —
|
|
8
|
+
# `$env:CI`. Specs therefore assert on the PowerShell that this gem generates
|
|
9
|
+
# rather than on whole-string equality, so they behave the same on a laptop
|
|
10
|
+
# and on a runner.
|
|
11
|
+
|
|
12
|
+
describe "plugin registration" do
|
|
13
|
+
it "declares provisioner API version 2" do
|
|
14
|
+
expect(provisioner.diagnose_plugin[:api_version]).to eq(2)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "exposes a writable tmp_dir" do
|
|
18
|
+
provisioner.tmp_dir = 'C:\\temp'
|
|
19
|
+
|
|
20
|
+
expect(provisioner.tmp_dir).to eq('C:\\temp')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "default configuration" do
|
|
25
|
+
it "stages configuration scripts out of examples/" do
|
|
26
|
+
expect(provisioner[:configuration_script_folder]).to eq("examples")
|
|
27
|
+
expect(provisioner[:configuration_script]).to eq("dsc_configuration.ps1")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "looks for DSC resources under modules/" do
|
|
31
|
+
expect(provisioner[:modules_path]).to eq("modules")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "targets WMF 4 and force-bootstraps NuGet" do
|
|
35
|
+
expect(provisioner[:dsc_local_configuration_manager_version]).to eq("wmf4")
|
|
36
|
+
expect(provisioner[:nuget_force_bootstrap]).to be(true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "when the suite is named" do
|
|
40
|
+
def suite_name
|
|
41
|
+
"webserver"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "derives the configuration name from the suite" do
|
|
45
|
+
expect(provisioner[:configuration_name]).to eq(["webserver"])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "#finalize_config!" do
|
|
51
|
+
# finalize_config! is called for us when KitchenHelpers builds the
|
|
52
|
+
# instance, so the merged LCM settings are already on config by this point.
|
|
53
|
+
|
|
54
|
+
it "replaces the LCM config with the resolved settings" do
|
|
55
|
+
expect(provisioner[:dsc_local_configuration_manager]).to include(
|
|
56
|
+
allow_module_overwrite: false,
|
|
57
|
+
configuration_mode: "ApplyAndAutoCorrect",
|
|
58
|
+
configuration_mode_frequency_mins: 30,
|
|
59
|
+
reboot_if_needed: false,
|
|
60
|
+
refresh_mode: "PUSH",
|
|
61
|
+
refresh_frequency_mins: 15
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "renders an unset certificate_id as a PowerShell null" do
|
|
66
|
+
expect(provisioner[:dsc_local_configuration_manager][:certificate_id]).to eq("$null")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "keeps caller-supplied LCM settings" do
|
|
70
|
+
provisioner = build_provisioner(
|
|
71
|
+
dsc_local_configuration_manager: {
|
|
72
|
+
configuration_mode: "ApplyOnly",
|
|
73
|
+
reboot_if_needed: true,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(provisioner[:dsc_local_configuration_manager]).to include(
|
|
78
|
+
configuration_mode: "ApplyOnly",
|
|
79
|
+
reboot_if_needed: true
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context "when targeting WMF 5" do
|
|
84
|
+
subject(:provisioner) { build_provisioner(dsc_local_configuration_manager_version: "wmf5") }
|
|
85
|
+
|
|
86
|
+
it "uses the WMF 5 defaults and settings" do
|
|
87
|
+
expect(provisioner[:dsc_local_configuration_manager]).to include(
|
|
88
|
+
action_after_reboot: "StopConfiguration",
|
|
89
|
+
debug_mode: "All",
|
|
90
|
+
configuration_mode_frequency_mins: 15,
|
|
91
|
+
refresh_frequency_mins: 30
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "#install_command" do
|
|
98
|
+
subject(:command) { provisioner.install_command }
|
|
99
|
+
|
|
100
|
+
it "silences the PowerShell progress stream" do
|
|
101
|
+
expect(command).to include("$ProgressPreference = 'SilentlyContinue';")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "declares and applies the SetupLCM configuration" do
|
|
105
|
+
expect(command).to include("configuration SetupLCM")
|
|
106
|
+
expect(command).to include("$null = SetupLCM")
|
|
107
|
+
expect(command).to include("Set-DscLocalConfigurationManager -Path ./SetupLCM | out-null")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "emits the resolved LCM settings" do
|
|
111
|
+
expect(command).to include("AllowModuleOverwrite = [bool]::Parse('false')")
|
|
112
|
+
expect(command).to include("ConfigurationMode = 'ApplyAndAutoCorrect'")
|
|
113
|
+
expect(command).to include("RefreshMode = 'PUSH'")
|
|
114
|
+
expect(command).to include("CertificateID = $null")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
context "with a certificate thumbprint" do
|
|
118
|
+
subject(:provisioner) do
|
|
119
|
+
build_provisioner(dsc_local_configuration_manager: { certificate_id: "ABC123" })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "single-quotes the thumbprint" do
|
|
123
|
+
expect(command).to include("CertificateID = 'ABC123'")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
context "when targeting WMF 5" do
|
|
128
|
+
subject(:provisioner) { build_provisioner(dsc_local_configuration_manager_version: "wmf5") }
|
|
129
|
+
|
|
130
|
+
it "emits the WMF 5 meta-configuration" do
|
|
131
|
+
expect(command).to include("[DSCLocalConfigurationManager()]")
|
|
132
|
+
expect(command).to include("ActionAfterReboot = 'StopConfiguration'")
|
|
133
|
+
expect(command).to include("DebugMode = 'All'")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context "when the LCM version is unrecognized" do
|
|
138
|
+
subject(:provisioner) { build_provisioner(dsc_local_configuration_manager_version: "wmf9000") }
|
|
139
|
+
|
|
140
|
+
# DscLcmConfiguration::Factory falls through to LcmBase for anything it
|
|
141
|
+
# does not recognize — including the "wmf4" default this gem ships.
|
|
142
|
+
it "falls back to the base LCM configuration" do
|
|
143
|
+
expect(command).to include("LocalConfigurationManager")
|
|
144
|
+
expect(command).not_to include("ActionAfterReboot")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "#init_command" do
|
|
150
|
+
subject(:command) { provisioner.init_command }
|
|
151
|
+
|
|
152
|
+
it "creates the remote configuration directory" do
|
|
153
|
+
expect(command).to include(
|
|
154
|
+
"mkdir (split-path (join-path #{KitchenHelpers::DEFAULT_ROOT_PATH} " \
|
|
155
|
+
"configuration/dsc_configuration.ps1)) -force | out-null"
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context "on WMF 4 with gallery modules requested" do
|
|
160
|
+
subject(:provisioner) { build_provisioner(modules_from_gallery: %w{xNetworking}) }
|
|
161
|
+
|
|
162
|
+
# Gallery installation needs PowerShellGet, which ships with WMF 5.
|
|
163
|
+
it "does not attempt to install them" do
|
|
164
|
+
expect(command).not_to include("install-module")
|
|
165
|
+
expect(command).not_to include("install-packageprovider")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context "on WMF 5 without gallery modules" do
|
|
170
|
+
subject(:provisioner) { build_provisioner(dsc_local_configuration_manager_version: "wmf5") }
|
|
171
|
+
|
|
172
|
+
it "does not attempt to install anything" do
|
|
173
|
+
expect(command).not_to include("install-module")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context "on WMF 5 with gallery modules" do
|
|
178
|
+
subject(:provisioner) do
|
|
179
|
+
build_provisioner(
|
|
180
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
181
|
+
modules_from_gallery: %w{xNetworking xWebAdministration}
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "bootstraps the NuGet package provider" do
|
|
186
|
+
expect(command).to include("install-packageprovider nuget -force -forcebootstrap | out-null")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "installs each module from the default gallery" do
|
|
190
|
+
expect(command).to include("install-module -name 'xNetworking' -Repository PSGallery -force | out-null")
|
|
191
|
+
expect(command).to include("install-module -name 'xWebAdministration' -Repository PSGallery -force | out-null")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "does not register a repository when no gallery_uri is given" do
|
|
195
|
+
expect(command).not_to include("register-packagesource")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
context "with nuget_force_bootstrap disabled" do
|
|
199
|
+
subject(:provisioner) do
|
|
200
|
+
build_provisioner(
|
|
201
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
202
|
+
modules_from_gallery: %w{xNetworking},
|
|
203
|
+
nuget_force_bootstrap: false
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "skips the package provider bootstrap" do
|
|
208
|
+
expect(command).not_to include("install-packageprovider")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context "with a private gallery_uri and no gallery_name" do
|
|
213
|
+
subject(:provisioner) do
|
|
214
|
+
build_provisioner(
|
|
215
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
216
|
+
modules_from_gallery: %w{xNetworking},
|
|
217
|
+
gallery_uri: "https://gallery.example.invalid/api/v2"
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "registers the source under the fallback name 'testing'" do
|
|
222
|
+
expect(command).to include(
|
|
223
|
+
"register-packagesource -providername PowerShellGet -name 'testing' " \
|
|
224
|
+
"-location 'https://gallery.example.invalid/api/v2' -force -trusted"
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "installs modules from that source" do
|
|
229
|
+
expect(command).to include("install-module -name 'xNetworking' -Repository testing -force | out-null")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
context "with a named private gallery" do
|
|
234
|
+
subject(:provisioner) do
|
|
235
|
+
build_provisioner(
|
|
236
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
237
|
+
modules_from_gallery: %w{xNetworking},
|
|
238
|
+
gallery_uri: "https://gallery.example.invalid/api/v2",
|
|
239
|
+
gallery_name: "Internal"
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "registers and installs under the given name" do
|
|
244
|
+
expect(command).to include("-name 'Internal'")
|
|
245
|
+
expect(command).to include("install-module -name 'xNetworking' -Repository Internal -force | out-null")
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
context "when a module is given as a specification hash" do
|
|
250
|
+
subject(:provisioner) do
|
|
251
|
+
build_provisioner(
|
|
252
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
253
|
+
modules_from_gallery: [{ "Name" => "xWebAdministration", "RequiredVersion" => "1.10.0.0" }]
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "passes each key through as a PowerShell parameter" do
|
|
258
|
+
expect(command).to include("install-module -Name xWebAdministration -RequiredVersion 1.10.0.0")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "defaults the repository to the resolved gallery" do
|
|
262
|
+
expect(command).to include("-repository PSGallery")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
context "when a module specification sets its own repository" do
|
|
267
|
+
subject(:provisioner) do
|
|
268
|
+
build_provisioner(
|
|
269
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
270
|
+
gallery_name: "Internal",
|
|
271
|
+
gallery_uri: "https://gallery.example.invalid/api/v2",
|
|
272
|
+
modules_from_gallery: [{ "Name" => "xWebAdministration", "Repository" => "Other" }]
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "does not override it with the configured gallery" do
|
|
277
|
+
expect(command).to include("-Repository Other")
|
|
278
|
+
expect(command).not_to include("-repository Internal")
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
context "when a module specification passes Force" do
|
|
283
|
+
subject(:provisioner) do
|
|
284
|
+
build_provisioner(
|
|
285
|
+
dsc_local_configuration_manager_version: "wmf5",
|
|
286
|
+
modules_from_gallery: [{ "Name" => "xWebAdministration", "Force" => true }]
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# -force is already appended to every install-module call; passing it
|
|
291
|
+
# twice is a PowerShell parameter-binding error.
|
|
292
|
+
it "drops it rather than emitting -Force twice" do
|
|
293
|
+
expect(command).to include("install-module -Name xWebAdministration -repository PSGallery -force")
|
|
294
|
+
expect(command).not_to include("-Force true")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
describe "#create_sandbox" do
|
|
301
|
+
let(:sandbox) { create_sandbox_for(provisioner) }
|
|
302
|
+
|
|
303
|
+
before { write_kitchen_file("examples/dsc_configuration.ps1", "configuration default {}\n") }
|
|
304
|
+
|
|
305
|
+
it "stages the configuration script under configuration/" do
|
|
306
|
+
expect(File.read(File.join(sandbox, "configuration/dsc_configuration.ps1")))
|
|
307
|
+
.to eq("configuration default {}\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "logs what it is staging" do
|
|
311
|
+
sandbox
|
|
312
|
+
|
|
313
|
+
expect(kitchen_log).to include("Staging DSC Resource Modules for copy to the SUT")
|
|
314
|
+
expect(kitchen_log).to include("Staging DSC configuration script for copy to the SUT")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
context "with a custom configuration script location" do
|
|
318
|
+
subject(:provisioner) do
|
|
319
|
+
build_provisioner(
|
|
320
|
+
configuration_script_folder: "dsc",
|
|
321
|
+
configuration_script: "web.ps1"
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
before { write_kitchen_file("dsc/web.ps1", "configuration web {}\n") }
|
|
326
|
+
|
|
327
|
+
it "stages it under its own name" do
|
|
328
|
+
expect(File.read(File.join(sandbox, "configuration/web.ps1"))).to eq("configuration web {}\n")
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
context "when the configuration script is missing" do
|
|
333
|
+
subject(:provisioner) { build_provisioner(configuration_script: "absent.ps1") }
|
|
334
|
+
|
|
335
|
+
it "fails loudly rather than staging an empty sandbox" do
|
|
336
|
+
expect { create_sandbox_for(provisioner) }.to raise_error(Errno::ENOENT, /absent\.ps1/)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
context "with a repository-style layout" do
|
|
341
|
+
before do
|
|
342
|
+
write_kitchen_file("modules/xExample/xExample.psd1")
|
|
343
|
+
write_kitchen_file("modules/xExample/DSCResources/xThing/xThing.psm1")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it "copies the modules directory into the sandbox" do
|
|
347
|
+
expect(files_under(File.join(sandbox, "modules"))).to contain_exactly(
|
|
348
|
+
"xExample/xExample.psd1",
|
|
349
|
+
"xExample/DSCResources/xThing/xThing.psm1"
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
context "and a custom modules_path" do
|
|
354
|
+
subject(:provisioner) { build_provisioner(modules_path: "dsc_resources") }
|
|
355
|
+
|
|
356
|
+
before { write_kitchen_file("dsc_resources/xOther/xOther.psd1") }
|
|
357
|
+
|
|
358
|
+
it "copies from that path instead" do
|
|
359
|
+
expect(files_under(File.join(sandbox, "modules"))).to include("xOther/xOther.psd1")
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
context "and no modules directory at all" do
|
|
364
|
+
subject(:provisioner) { build_provisioner(modules_path: "does_not_exist") }
|
|
365
|
+
|
|
366
|
+
it "stages the configuration script anyway" do
|
|
367
|
+
expect(File).to exist(File.join(sandbox, "configuration/dsc_configuration.ps1"))
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "says why it skipped the copy" do
|
|
371
|
+
sandbox
|
|
372
|
+
|
|
373
|
+
expect(kitchen_log).to include("was not found. Not moving to")
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
context "with a module-style layout" do
|
|
379
|
+
# A <root>/<basename>.psd1 manifest is what flips the provisioner from
|
|
380
|
+
# repository style to module style.
|
|
381
|
+
let(:module_name) { File.basename(kitchen_root) }
|
|
382
|
+
|
|
383
|
+
before do
|
|
384
|
+
write_kitchen_file("#{module_name}.psd1", "@{ ModuleVersion = '1.0' }\n")
|
|
385
|
+
write_kitchen_file("#{module_name}.psm1", "function Get-Thing {}\n")
|
|
386
|
+
write_kitchen_file("DSCResources/xThing/xThing.psm1")
|
|
387
|
+
write_kitchen_file("Gemfile", "source 'https://rubygems.org'\n")
|
|
388
|
+
write_kitchen_file("Gemfile.lock", "DEPENDENCIES\n")
|
|
389
|
+
write_kitchen_file("README.md", "# docs\n")
|
|
390
|
+
write_kitchen_file("LICENSE.txt", "Apache-2.0\n")
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it "stages the whole module under modules/<module name>" do
|
|
394
|
+
expect(files_under(File.join(sandbox, "modules", module_name))).to contain_exactly(
|
|
395
|
+
"#{module_name}.psd1",
|
|
396
|
+
"#{module_name}.psm1",
|
|
397
|
+
"DSCResources/xThing/xThing.psm1",
|
|
398
|
+
"examples/dsc_configuration.ps1"
|
|
399
|
+
)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it "leaves repository housekeeping files behind" do
|
|
403
|
+
staged = files_under(File.join(sandbox, "modules", module_name))
|
|
404
|
+
|
|
405
|
+
expect(staged).not_to include("Gemfile", "Gemfile.lock", "README.md", "LICENSE.txt")
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
describe "#prepare_command" do
|
|
411
|
+
subject(:command) { provisioner.prepare_command }
|
|
412
|
+
|
|
413
|
+
it "copies staged modules onto the PSModulePath" do
|
|
414
|
+
expect(command).to include("if (Test-Path (join-path #{KitchenHelpers::DEFAULT_ROOT_PATH} 'modules'))")
|
|
415
|
+
expect(command).to include("copy-item -destination $env:programfiles/windowspowershell/modules/ -recurse -force")
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "fails on the SUT when the configuration script did not arrive" do
|
|
419
|
+
expect(command).to include('throw "Failed to find $ConfigurationScriptPath"')
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it "dot-sources the configuration script" do
|
|
423
|
+
expect(command).to include("invoke-expression (get-content $ConfigurationScriptPath -raw)")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
it "clears $Error and compiles the MOF for the suite's configuration" do
|
|
427
|
+
expect(command).to include("$Error.clear()")
|
|
428
|
+
expect(command).to include("$null = default -outputpath c:/configurations/default")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it "removes any MOF left over from a previous converge" do
|
|
432
|
+
expect(command).to include("Remove-Item -Recurse -Force c:/configurations/default")
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it "fails when the configuration command was never defined" do
|
|
436
|
+
expect(command).to include('throw "Failed to create a configuration command default"')
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it "logs the configuration it is compiling" do
|
|
440
|
+
command
|
|
441
|
+
|
|
442
|
+
expect(kitchen_log).to include("Generating the MOF script for the configuration default")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
context "with several configuration names" do
|
|
446
|
+
subject(:provisioner) { build_provisioner(configuration_name: %w{web database}) }
|
|
447
|
+
|
|
448
|
+
it "emits a compile block per configuration" do
|
|
449
|
+
expect(command).to include("$null = web -outputpath c:/configurations/web")
|
|
450
|
+
expect(command).to include("$null = database -outputpath c:/configurations/database")
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
context "with a single configuration name given as a string" do
|
|
455
|
+
subject(:provisioner) { build_provisioner(configuration_name: "web") }
|
|
456
|
+
|
|
457
|
+
it "treats it as a one-element list" do
|
|
458
|
+
expect(command).to include("$null = web -outputpath c:/configurations/web")
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
context "with configuration data" do
|
|
463
|
+
subject(:provisioner) do
|
|
464
|
+
build_provisioner(
|
|
465
|
+
configuration_data: {
|
|
466
|
+
"AllNodes" => [{ "NodeName" => "*", "PSDscAllowPlainTextPassword" => true }],
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
it "assigns it to a PowerShell hashtable before compiling" do
|
|
472
|
+
expect(command).to include("$ConfigurationData = @{")
|
|
473
|
+
expect(command).to include('"AllNodes" =')
|
|
474
|
+
expect(command).to include('"NodeName" = "*"')
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it "renders nested arrays as PowerShell arrays" do
|
|
478
|
+
expect(command).to include("@(")
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# ps_hash stringifies every scalar, so Ruby booleans reach DSC quoted.
|
|
482
|
+
it "renders booleans as quoted strings" do
|
|
483
|
+
expect(command).to include('"PSDscAllowPlainTextPassword" = "true"')
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it "passes the hashtable to the configuration" do
|
|
487
|
+
expect(command).to include("-configurationdata $ConfigurationData")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# `configuration_data_variable:` with an empty value in kitchen.yml parses
|
|
491
|
+
# as nil, which must not produce a bare `$` in the generated script.
|
|
492
|
+
context "with the variable name explicitly blanked out" do
|
|
493
|
+
subject(:provisioner) do
|
|
494
|
+
build_provisioner(
|
|
495
|
+
configuration_data: { "AllNodes" => [] },
|
|
496
|
+
configuration_data_variable: nil
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
it "falls back to $ConfigurationData" do
|
|
501
|
+
expect(command).to include("$ConfigurationData = @{")
|
|
502
|
+
expect(command).to include("-configurationdata $ConfigurationData")
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
context "under a custom variable name" do
|
|
507
|
+
subject(:provisioner) do
|
|
508
|
+
build_provisioner(
|
|
509
|
+
configuration_data: { "AllNodes" => [] },
|
|
510
|
+
configuration_data_variable: "MyData"
|
|
511
|
+
)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it "assigns and passes that variable" do
|
|
515
|
+
expect(command).to include("$MyData = @{")
|
|
516
|
+
expect(command).to include("-configurationdata $MyData")
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
context "without configuration data" do
|
|
522
|
+
it "assigns nothing" do
|
|
523
|
+
expect(command).not_to include("$ConfigurationData = @{")
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Known wart: the -configurationdata argument is emitted unconditionally,
|
|
527
|
+
# so the compile references an undefined variable. DSC tolerates it
|
|
528
|
+
# because $null is a valid ConfigurationData value.
|
|
529
|
+
it "still passes the (undefined) configuration data variable" do
|
|
530
|
+
expect(command).to include("-configurationdata $ConfigurationData")
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
describe "#run_command" do
|
|
536
|
+
subject(:command) { provisioner.run_command }
|
|
537
|
+
|
|
538
|
+
it "starts and waits on the DSC configuration job" do
|
|
539
|
+
expect(command).to include("$job = start-dscconfiguration -Path c:/configurations/default -force")
|
|
540
|
+
expect(command).to include("$job | wait-job")
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
it "surfaces the job's verbose stream" do
|
|
544
|
+
expect(command).to include("$verbose_output = $job.childjobs[0].verbose")
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it "reboots and exits 35 when DSC asks for a reboot" do
|
|
548
|
+
expect(command).to include("shutdown /r /t 15")
|
|
549
|
+
expect(command).to include("exit 35")
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
it "exits non-zero when the job reported errors" do
|
|
553
|
+
expect(command).to include("$dsc_errors = $job.childjobs[0].Error")
|
|
554
|
+
expect(command).to include("exit 1")
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it "logs the configuration it is applying" do
|
|
558
|
+
command
|
|
559
|
+
|
|
560
|
+
expect(kitchen_log).to include("Running the configuration default")
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it "retries on the reboot exit code" do
|
|
564
|
+
command
|
|
565
|
+
|
|
566
|
+
expect(provisioner[:retry_on_exit_code]).to eq([35])
|
|
567
|
+
expect(provisioner[:max_retries]).to eq(3)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
context "when the user configured their own retry behaviour" do
|
|
571
|
+
subject(:provisioner) { build_provisioner(retry_on_exit_code: [1, 2], max_retries: 5) }
|
|
572
|
+
|
|
573
|
+
it "leaves it alone" do
|
|
574
|
+
command
|
|
575
|
+
|
|
576
|
+
expect(provisioner[:retry_on_exit_code]).to eq([1, 2])
|
|
577
|
+
expect(provisioner[:max_retries]).to eq(5)
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
context "with several configuration names" do
|
|
582
|
+
subject(:provisioner) { build_provisioner(configuration_name: %w{web database}) }
|
|
583
|
+
|
|
584
|
+
it "applies each configuration in turn" do
|
|
585
|
+
expect(command).to include("start-dscconfiguration -Path c:/configurations/web -force")
|
|
586
|
+
expect(command).to include("start-dscconfiguration -Path c:/configurations/database -force")
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Kitchen::Dsc do
|
|
4
|
+
describe "VERSION" do
|
|
5
|
+
subject(:version) { described_class::VERSION }
|
|
6
|
+
|
|
7
|
+
it "is a frozen string" do
|
|
8
|
+
expect(version).to be_a(String).and be_frozen
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "is a dotted release number" do
|
|
12
|
+
expect(version).to match(/\A\d+\.\d+\.\d+(?:\.[A-Za-z0-9]+)*\z/)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "matches the version the gemspec builds against" do
|
|
16
|
+
gemspec = Gem::Specification.load(File.expand_path("../../kitchen-dsc.gemspec", __dir__))
|
|
17
|
+
|
|
18
|
+
expect(gemspec.version.to_s).to eq(version)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|