kitchen-azurerm 2.1.4 → 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65f0cc54a62e738484f2f5469a37e476e0323bdd0ac59ef180e99a05de3fa0c8
4
- data.tar.gz: dce34ba2cec4675d575aa4607f9b00874d4e504f8d5c7ee1ce1b18d1311b4434
3
+ metadata.gz: f2e163e7f873cd30975117e7959c9f860980e67c6a57a57cceea3b5784a15c88
4
+ data.tar.gz: 842ce181806cd18fd1ce064222311cb24f7702b5c569f9e57c1971ef0e8a2843
5
5
  SHA512:
6
- metadata.gz: cdf17e7f3013d461390749209072afbf0a7f8ef7969853467a66cd7b90471226b9848a36d89212c7148681cfcb27aa96bb5ac7d3e2e9e9ca38ac3d8839d7d987
7
- data.tar.gz: 25d1728326d14b31470120ee867263fbf814ffe6758946705b64391c7948031fbefbbafc123baa23ea4f404089d16d30cecf3117c2bc9867a1e0dfab2fac33d3
6
+ metadata.gz: 57922ce563572966e695342e34e01eb89c04bb30528c0931baa41b28bcf043ed2d6ccbd45141df8febbbc1739b038d9307ed666b0bd349b33b626df685f2ca70
7
+ data.tar.gz: 44e874be227fa2625d1b73091c80b1402e548b5bf7851da030737d80f58f479409585c7b5248d95c67e42d536bbada211127612cd037a38bf4b2257b214c66bc
@@ -25,6 +25,18 @@ module Kitchen
25
25
  # @return [String]
26
26
  NETWORK_API_VERSION = "2025-07-01".freeze
27
27
 
28
+ # ARM API version used for compute resource requests. Matches the
29
+ # version the bundled deployment templates declare for virtual
30
+ # machines.
31
+ #
32
+ # @return [String]
33
+ COMPUTE_API_VERSION = "2025-04-01".freeze
34
+
35
+ # ARM API version used to read the subscription itself.
36
+ #
37
+ # @return [String]
38
+ SUBSCRIPTION_API_VERSION = "2022-12-01".freeze
39
+
28
40
  # @param subscription_id [String]
29
41
  # @param environment [Environments::Environment]
30
42
  # @param token_provider [TokenProvider]
@@ -43,6 +55,19 @@ module Kitchen
43
55
  # @return [TokenProvider]
44
56
  attr_reader :token_provider
45
57
 
58
+ # Reads the subscription itself.
59
+ #
60
+ # Unlike a resource group HEAD - which answers 404 both for a
61
+ # subscription that does not exist and for one that merely has no such
62
+ # group - this distinguishes a reachable subscription from a wrong
63
+ # one, so it is what {Azurerm#doctor} probes with.
64
+ #
65
+ # @return [Hash]
66
+ def subscription
67
+ call(:get, "/subscriptions/#{escape(subscription_id)}",
68
+ api_version: SUBSCRIPTION_API_VERSION).json
69
+ end
70
+
46
71
  # Whether a resource group exists.
47
72
  #
48
73
  # @param name [String] resource group name, case-insensitive.
@@ -115,6 +140,17 @@ module Kitchen
115
140
  payload.is_a?(Hash) ? Array(payload["value"]) : []
116
141
  end
117
142
 
143
+ # Reads a virtual machine's instance view, which carries its current
144
+ # power state.
145
+ #
146
+ # @param resource_group [String]
147
+ # @param name [String] virtual machine name.
148
+ # @return [Hash]
149
+ def virtual_machine_instance_view(resource_group, name)
150
+ call(:get, "#{compute_path(resource_group, "virtualMachines", name)}/instanceView",
151
+ api_version: COMPUTE_API_VERSION).json
152
+ end
153
+
118
154
  # Reads a public IP address resource.
119
155
  #
120
156
  # @param resource_group [String]
@@ -158,6 +194,14 @@ module Kitchen
158
194
  "#{resource_group_path(resource_group)}/providers/Microsoft.Network/#{type}/#{escape(name)}"
159
195
  end
160
196
 
197
+ # @param resource_group [String]
198
+ # @param type [String] e.g. +"virtualMachines"+.
199
+ # @param name [String]
200
+ # @return [String]
201
+ def compute_path(resource_group, type, name)
202
+ "#{resource_group_path(resource_group)}/providers/Microsoft.Compute/#{type}/#{escape(name)}"
203
+ end
204
+
161
205
  # @param value [String]
162
206
  # @return [String] path-escaped
163
207
  def escape(value)
@@ -63,6 +63,10 @@ module Kitchen
63
63
  # Every supported cloud, keyed by its downcased name so that lookups are
64
64
  # case-insensitive.
65
65
  #
66
+ # +AzureGermanCloud+ is kept only so that an existing kitchen.yml naming
67
+ # it still loads: Microsoft closed that cloud in 2021, and its endpoints
68
+ # no longer answer.
69
+ #
66
70
  # @return [Hash{String => Environment}]
67
71
  ALL = [
68
72
  Environment.new("Azure",
@@ -22,11 +22,12 @@ module Kitchen
22
22
 
23
23
  # The HTTP status code of the failing response.
24
24
  #
25
- # @return [Integer]
25
+ # @return [Integer, nil] nil when the error was raised before a
26
+ # response arrived, as the token providers do.
26
27
  attr_reader :status
27
28
 
28
29
  # @param message [String] human-readable summary.
29
- # @param status [Integer] HTTP status code.
30
+ # @param status [Integer, nil] HTTP status code, when there was one.
30
31
  # @param body [Hash] parsed ARM error body.
31
32
  def initialize(message, status: nil, body: {})
32
33
  super(message)
@@ -40,6 +41,17 @@ module Kitchen
40
41
  def code
41
42
  body.is_a?(Hash) ? body.dig("error", "code") : nil
42
43
  end
44
+
45
+ # Azure's own explanation, when ARM supplied one.
46
+ #
47
+ # +message+ is this class's summary of the request that failed; this
48
+ # is what Azure said about it, which is usually the part worth showing
49
+ # somebody.
50
+ #
51
+ # @return [String, nil]
52
+ def detail
53
+ body.is_a?(Hash) ? body.dig("error", "message") : nil
54
+ end
43
55
  end
44
56
 
45
57
  # Raised when a request could not be completed and is worth retrying:
@@ -2,6 +2,7 @@ require "kitchen"
2
2
 
3
3
  require_relative "azure_credentials"
4
4
  require_relative "azure/errors"
5
+ require_relative "azurerm_version"
5
6
  require "securerandom" unless defined?(SecureRandom)
6
7
  require "base64" unless defined?(Base64)
7
8
  autoload :SSHKey, "sshkey"
@@ -29,8 +30,21 @@ module Kitchen
29
30
  # @return [Azure::ArmClient, nil]
30
31
  attr_accessor :arm_client
31
32
 
33
+ # The writer needs a docstring of its own: one comment above an
34
+ # attr_accessor is copied to both methods, so the argument the writer
35
+ # takes can only be described here. The directive has to come after the
36
+ # accessor - placed before it, the accessor overwrites it.
37
+
38
+ # @!method arm_client=(client)
39
+ # Replaces the ARM client.
40
+ #
41
+ # @param client [Azure::ArmClient, nil] the client to use.
42
+ # @return [Azure::ArmClient, nil] the client that was set.
43
+
32
44
  kitchen_driver_api_version 2
33
45
 
46
+ plugin_version Kitchen::Driver::AZURERM_VERSION
47
+
34
48
  # Settings that Azure retirements have made inoperable. They are still
35
49
  # accepted so that an existing kitchen.yml keeps loading, but they no
36
50
  # longer do anything and {#warn_about_deprecated_config} says so.
@@ -66,8 +80,12 @@ module Kitchen
66
80
 
67
81
  # Ubuntu 22.04 LTS, generation 2. Canonical renamed their offers after
68
82
  # 18.04, so the old "UbuntuServer" offer no longer resolves at all.
83
+ #
84
+ # @return [String]
85
+ DEFAULT_IMAGE_URN = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest".freeze
86
+
69
87
  default_config(:image_urn) do |_config|
70
- "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest"
88
+ DEFAULT_IMAGE_URN
71
89
  end
72
90
 
73
91
  default_config(:image_id) do |_config|
@@ -249,6 +267,8 @@ module Kitchen
249
267
  # @param state [Hash] the instance state, mutated in place.
250
268
  # @return [void]
251
269
  # @raise [RuntimeError] if no +subscription_id+ can be resolved.
270
+ # @raise [Kitchen::UserError] if +os_disk_size_gb+ is not a whole number,
271
+ # or the public half of the transport's SSH key cannot be found.
252
272
  # @raise [Azure::OperationError] if an Azure API call fails
253
273
  # for any reason other than an already-running deployment.
254
274
  def create(state)
@@ -273,12 +293,12 @@ module Kitchen
273
293
  end
274
294
 
275
295
  begin
276
- run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template])
296
+ run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template].to_s)
277
297
 
278
298
  run_deployment(state, "deploy", deployment(deployment_parameters))
279
299
  store_deployment_credentials(state, deployment_parameters)
280
300
 
281
- run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template])
301
+ run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template].to_s)
282
302
  rescue Azure::OperationError => operation_error
283
303
  info operation_error.body["error"]
284
304
  raise operation_error
@@ -287,6 +307,79 @@ module Kitchen
287
307
  state[:hostname] = resolve_hostname(state, deployment_parameters["nicName"])
288
308
  end
289
309
 
310
+ # Settings the driver cannot supply a default for, and what they are.
311
+ #
312
+ # @return [Hash{Symbol => String}]
313
+ REQUIRED_CONFIG = {
314
+ subscription_id: "the Azure subscription to deploy into",
315
+ location: "the Azure region to deploy into, e.g. eastus",
316
+ machine_size: "the VM size to deploy, e.g. Standard_D2s_v3",
317
+ }.freeze
318
+
319
+ # Checks configuration and credentials, for +kitchen doctor+.
320
+ #
321
+ # Deployment failures are usually one of two things: a required setting
322
+ # nobody filled in, or credentials that do not work. Both otherwise
323
+ # surface as an Azure error partway through a create, once the resource
324
+ # group already exists, so this looks for them up front.
325
+ #
326
+ # @param _state [Hash] the instance state. Test Kitchen passes it, but
327
+ # the checks are about configuration and credentials, which exist
328
+ # before any instance does, so it is ignored.
329
+ # @return [Boolean] true when a problem was found, as +kitchen doctor+
330
+ # expects.
331
+ def doctor(_state)
332
+ problems = missing_required_config
333
+ problems += unreachable_azure if problems.empty?
334
+ problems.each { |problem| error("kitchen-azurerm: #{problem}") }
335
+ problems.any?
336
+ end
337
+
338
+ # Required settings that were left unset.
339
+ #
340
+ # @return [Array<String>]
341
+ def missing_required_config
342
+ REQUIRED_CONFIG.select { |option, _| config[option].to_s.empty? }
343
+ .map { |option, purpose| "#{option} is not set. It has no default: give it #{purpose}." }
344
+ end
345
+
346
+ # Whether Azure answers, accepts the credentials, and knows the
347
+ # subscription.
348
+ #
349
+ # Reads the subscription rather than probing a resource group: ARM
350
+ # answers a resource group HEAD with 404 both for a subscription that
351
+ # does not exist and for one that merely has no such group, so it cannot
352
+ # tell a wrong subscription from a healthy one. Reading the subscription
353
+ # gives back either its details or +SubscriptionNotFound+.
354
+ #
355
+ # @return [Array<String>]
356
+ def unreachable_azure
357
+ Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id],
358
+ environment: config[:azure_environment]).arm_client.subscription
359
+ []
360
+ rescue Azure::OperationError => operation_error
361
+ [azure_problem(operation_error)]
362
+ rescue Azure::TransientError => transient_error
363
+ ["Could not reach Azure (#{transient_error.message})."]
364
+ end
365
+
366
+ # Describes an OperationError raised while checking Azure.
367
+ #
368
+ # An error carrying no Azure error code never reached ARM - it comes
369
+ # from acquiring the token, and already explains itself. Announcing that
370
+ # Azure rejected a request that was never made only sends the reader
371
+ # looking in the wrong place.
372
+ #
373
+ # @param operation_error [Azure::OperationError]
374
+ # @return [String]
375
+ def azure_problem(operation_error)
376
+ return operation_error.message unless operation_error.code
377
+
378
+ "Azure rejected the request (#{operation_error.code}: " \
379
+ "#{operation_error.detail || operation_error.message}). " \
380
+ "Check the credentials, and that they can reach subscription #{config[:subscription_id]}."
381
+ end
382
+
290
383
  # Builds the ARM parameter values for the virtual machine deployment.
291
384
  #
292
385
  # @param state [Hash] instance state, already through {#validate_state}.
@@ -329,10 +422,23 @@ module Kitchen
329
422
  def image_parameters
330
423
  return { "imageId" => config[:image_id] } if config[:image_id].to_s != ""
331
424
 
332
- publisher, offer, sku, version = config[:image_urn].split(":", 4)
425
+ publisher, offer, sku, version = image_urn.split(":", 4)
333
426
  { "imagePublisher" => publisher, "imageOffer" => offer, "imageSku" => sku, "imageVersion" => version }
334
427
  end
335
428
 
429
+ # The Marketplace image URN to deploy from.
430
+ #
431
+ # An +image_urn:+ written with no value is nil rather than "", which
432
+ # counts as unset here the way it does everywhere else in the driver -
433
+ # so it falls back to {DEFAULT_IMAGE_URN} rather than failing with a
434
+ # Ruby error naming neither the setting nor the file it came from.
435
+ #
436
+ # @return [String]
437
+ def image_urn
438
+ urn = config[:image_urn].to_s
439
+ urn.empty? ? DEFAULT_IMAGE_URN : urn
440
+ end
441
+
336
442
  # The OS disk size, as a number ARM will accept.
337
443
  #
338
444
  # YAML makes +os_disk_size_gb: 64+ an Integer and +os_disk_size_gb: "64"+
@@ -451,14 +557,29 @@ module Kitchen
451
557
 
452
558
  # Whether a state property is already populated.
453
559
  #
560
+ # A blank value does not count. A run that fails early still writes its
561
+ # state, so an empty +subscription_id+ - which is what a quoted ERB
562
+ # interpolation of an unset variable produces - used to be taken as
563
+ # authoritative from then on, shadowing the real value in config for the
564
+ # rest of the instance's life.
565
+ #
566
+ # +false+ is a value, so this asks whether the value is blank rather
567
+ # than whether it is truthy.
568
+ #
454
569
  # @param state [Hash] Hash of existing state values.
455
570
  # @param property [Symbol, String] the property to check.
456
- # @return [Boolean] true when the key exists and its value is not nil.
571
+ # @return [Boolean] true when the key exists and its value is not blank.
457
572
  def existing_state_value?(state, property)
458
- state.key?(property) && !state[property].nil?
573
+ state.key?(property) && !state[property].to_s.empty?
459
574
  end
460
575
 
461
- # Fills in any state values that are not already present.
576
+ # Fills in any state values that are not already present, and drops the
577
+ # generated password when the transport authenticates with an SSH key.
578
+ #
579
+ # +use_managed_disks+ is copied across with the settings that are still
580
+ # read, even though Azure's retirement of unmanaged disks left nothing
581
+ # reading it - see {DEPRECATED_CONFIG}. It is written as nil, since the
582
+ # setting has no default.
462
583
  #
463
584
  # @param state [Hash] existing Hash of state values.
464
585
  # @return [Hash] the same Hash, with defaults applied.
@@ -605,7 +726,12 @@ module Kitchen
605
726
  KEY_GENERATION_MUTEX.synchronize do
606
727
  generate_key_pair(private_key_filename) unless File.file?(private_key_filename)
607
728
 
608
- public_key_filename = instance.transport[:ssh_public_key] || "#{private_key_filename}.pub"
729
+ explicit = instance.transport[:ssh_public_key]
730
+ public_key_filename = explicit || "#{private_key_filename}.pub"
731
+ unless File.file?(public_key_filename)
732
+ raise Kitchen::UserError, missing_public_key_message(private_key_filename, public_key_filename, explicit)
733
+ end
734
+
609
735
  File.read(public_key_filename).strip
610
736
  end
611
737
  end
@@ -626,6 +752,33 @@ module Kitchen
626
752
  File.chmod(0600, "#{private_key_filename}.pub")
627
753
  end
628
754
 
755
+ # Explains that the public half of the transport's key could not be found.
756
+ #
757
+ # +ssh_key+ names the *private* key, so a user who keeps only that has
758
+ # nothing wrong with their kitchen.yml. They used to get a raw
759
+ # Errno::ENOENT for a ".pub" path they never wrote, with nothing to say
760
+ # where it came from or what to do about it.
761
+ #
762
+ # The key cannot simply be derived: sshkey only reads PEM-encoded RSA,
763
+ # while ssh-keygen has emitted the OpenSSH format by default since 7.8
764
+ # and ed25519 keys are never PEM. +ssh-keygen -y+ handles every format,
765
+ # so point at that instead.
766
+ #
767
+ # @param private_key_filename [String] the configured private key.
768
+ # @param public_key_filename [String] where the public key was looked for.
769
+ # @param explicit [String, nil] +ssh_public_key+, when the user set it.
770
+ # @return [String]
771
+ def missing_public_key_message(private_key_filename, public_key_filename, explicit)
772
+ if explicit
773
+ "The transport's ssh_public_key setting points at #{public_key_filename}, which does not exist. " \
774
+ "Correct the path, or remove the setting to use #{private_key_filename}.pub."
775
+ else
776
+ "No public key was found at #{public_key_filename}. The transport's ssh_key setting names the " \
777
+ "private key, and the public half of it has to go on the virtual machine. Create it with: " \
778
+ "ssh-keygen -y -f #{private_key_filename} > #{public_key_filename}"
779
+ end
780
+ end
781
+
629
782
  # Builds the pre-deployment from a caller-supplied ARM template file.
630
783
  #
631
784
  # @param pre_deployment_template_filename [String] path to an ARM template.
@@ -772,6 +925,52 @@ module Kitchen
772
925
  end
773
926
  end
774
927
 
928
+ # How Azure's power states map onto Test Kitchen's liveness question.
929
+ #
930
+ # Anything absent from this table is passed through as-is with +live+
931
+ # left nil: reporting a state we have not seen before is more use than
932
+ # guessing whether it counts as running.
933
+ #
934
+ # @return [Hash{String => Boolean}]
935
+ POWER_STATES = {
936
+ "running" => true,
937
+ "starting" => true,
938
+ "stopping" => true,
939
+ "stopped" => false,
940
+ "deallocating" => false,
941
+ "deallocated" => false,
942
+ }.freeze
943
+
944
+ # Reports whether the virtual machine backing this instance is running.
945
+ #
946
+ # Drives +kitchen list --live+ (and its +kitchen status+ alias). Test
947
+ # Kitchen rescues anything raised here and reports "unknown", but a
948
+ # listing should not be where an Azure outage first shows up, so the
949
+ # failure modes are handled explicitly and reported as data.
950
+ #
951
+ # No retries: this is a status probe behind an interactive command, and
952
+ # waiting out the retry budget on every unreachable instance would be
953
+ # worse than saying so promptly.
954
+ #
955
+ # @param state [Hash] the instance state.
956
+ # @return [Hash] always +:live+, +:state+ and +:message+; +:resource_id+
957
+ # only when the virtual machine was found, since nothing else knows
958
+ # which resource to name.
959
+ def status(state)
960
+ resource_group = state[:azure_resource_group_name]
961
+ vm_name = state[:vm_name]
962
+ return { live: false, state: "not_created", message: "No Azure virtual machine has been created yet." } unless
963
+ state[:server_id] && resource_group && vm_name
964
+
965
+ power_state_status(state, resource_group, vm_name)
966
+ rescue Azure::OperationError => operation_error
967
+ return { live: false, state: "not_created", message: "The virtual machine no longer exists in Azure." } if operation_error.status == 404
968
+
969
+ { live: nil, state: "unknown", message: "#{operation_error.code}: #{operation_error.message}" }
970
+ rescue Azure::TransientError => transient_error
971
+ { live: nil, state: "unknown", message: "Could not reach Azure (#{transient_error.message})." }
972
+ end
973
+
775
974
  # Tears down whatever {#create} built.
776
975
  #
777
976
  # @param state [Hash] the instance state, mutated in place.
@@ -779,8 +978,8 @@ module Kitchen
779
978
  # @raise [Azure::OperationError] if an Azure API call fails.
780
979
  def destroy(state)
781
980
  # TODO: We have some not so fun state issues we need to clean up
782
- state[:azure_environment] = config[:azure_environment] unless state[:azure_environment]
783
- state[:subscription_id] = config[:subscription_id] unless state[:subscription_id]
981
+ state[:azure_environment] = config[:azure_environment] unless existing_state_value?(state, :azure_environment)
982
+ state[:subscription_id] = config[:subscription_id] unless existing_state_value?(state, :subscription_id)
784
983
 
785
984
  @arm_client = Kitchen::Driver::AzureCredentials.new(subscription_id: state[:subscription_id],
786
985
  environment: state[:azure_environment]).arm_client
@@ -816,6 +1015,53 @@ module Kitchen
816
1015
  state.delete(:password)
817
1016
  end
818
1017
 
1018
+ # Asks Azure for the virtual machine's power state.
1019
+ #
1020
+ # @param state [Hash] the instance state.
1021
+ # @param resource_group [String]
1022
+ # @param vm_name [String]
1023
+ # @return [Hash] as {#status} returns.
1024
+ def power_state_status(state, resource_group, vm_name)
1025
+ view = status_arm_client(state).virtual_machine_instance_view(resource_group, vm_name)
1026
+ status = Array(view["statuses"]).find { |entry| entry["code"].to_s.start_with?("PowerState/") }
1027
+ power = status && status["code"].to_s.split("/", 2).last
1028
+
1029
+ {
1030
+ live: power ? POWER_STATES[power] : nil,
1031
+ state: power || "unknown",
1032
+ resource_id: virtual_machine_id(status_subscription_id(state), resource_group, vm_name),
1033
+ message: status && status["displayStatus"],
1034
+ }
1035
+ end
1036
+
1037
+ # An ARM client for a status probe, built from state the way {#destroy}
1038
+ # builds one, so that an instance created against another subscription
1039
+ # or cloud is still asked about in the right place.
1040
+ #
1041
+ # @param state [Hash] the instance state.
1042
+ # @return [Azure::ArmClient]
1043
+ def status_arm_client(state)
1044
+ @arm_client ||= Kitchen::Driver::AzureCredentials.new(
1045
+ subscription_id: status_subscription_id(state),
1046
+ environment: state[:azure_environment] || config[:azure_environment]
1047
+ ).arm_client
1048
+ end
1049
+
1050
+ # @param state [Hash] the instance state.
1051
+ # @return [String, nil] the subscription the instance was created in.
1052
+ def status_subscription_id(state)
1053
+ state[:subscription_id] || config[:subscription_id]
1054
+ end
1055
+
1056
+ # @param subscription_id [String]
1057
+ # @param resource_group [String]
1058
+ # @param vm_name [String]
1059
+ # @return [String] the virtual machine's ARM resource id.
1060
+ def virtual_machine_id(subscription_id, resource_group, vm_name)
1061
+ "/subscriptions/#{subscription_id}/resourceGroups/#{resource_group}" \
1062
+ "/providers/Microsoft.Compute/virtualMachines/#{vm_name}"
1063
+ end
1064
+
819
1065
  # Deletes an explicitly-named resource group when the instance itself was
820
1066
  # never created but the user asked for the group to be removed.
821
1067
  #
@@ -839,7 +1085,15 @@ module Kitchen
839
1085
  end
840
1086
 
841
1087
  # Empties a resource group by deploying an empty template in Complete
842
- # mode, then restores or clears the group's tags per configuration.
1088
+ # mode, then clears the group's tags unless asked to keep them.
1089
+ #
1090
+ # An empty Complete-mode deployment removes the resources in the group
1091
+ # and leaves the group's own tags alone, so keeping them means leaving
1092
+ # the group be. Rewriting it is what used to remove them: ARM's PUT on a
1093
+ # resource group replaces its tags rather than merging them, so putting
1094
+ # the *configured* tags back overwrote whatever the group actually
1095
+ # carried - which, with +resource_group_tags+ unset, meant erasing them
1096
+ # while announcing they would be kept.
843
1097
  #
844
1098
  # @param state [Hash] the instance state.
845
1099
  # @return [void]
@@ -850,11 +1104,11 @@ module Kitchen
850
1104
 
851
1105
  if config[:destroy_explicit_resource_group_tags] == false
852
1106
  warn 'The "destroy_explicit_resource_group_tags" setting value is set to "false". The tags on the resource group will NOT be removed.'
853
- create_resource_group(state[:azure_resource_group_name], get_resource_group)
854
- else
855
- warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
856
- create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
1107
+ return
857
1108
  end
1109
+
1110
+ warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
1111
+ create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
858
1112
  rescue Azure::OperationError => operation_error
859
1113
  error operation_error.body
860
1114
  raise operation_error
@@ -1162,7 +1416,11 @@ module Kitchen
1162
1416
 
1163
1417
  # Base64-encoded custom data for the VM.
1164
1418
  #
1165
- # @return [String, nil] nil when no +custom_data+ is configured.
1419
+ # An unset +custom_data+ is "" rather than nil, which encodes to "" - so
1420
+ # callers guard on the setting being empty rather than on this returning
1421
+ # nil.
1422
+ #
1423
+ # @return [String, nil] nil only when +custom_data+ is explicitly nil.
1166
1424
  def prepared_custom_data
1167
1425
  return nil if config[:custom_data].nil?
1168
1426
 
@@ -1311,7 +1569,7 @@ module Kitchen
1311
1569
  # Requests deletion of a resource group without waiting for it to finish.
1312
1570
  #
1313
1571
  # @param resource_group_name [String] the resource group name.
1314
- # @return [Object] the operation response.
1572
+ # @return [void] ARM answers a delete with no body worth returning.
1315
1573
  def delete_resource_group_async(resource_group_name)
1316
1574
  with_azure_retries("while sending resource group deletion request for '#{resource_group_name}'.") do
1317
1575
  arm_client.delete_resource_group(resource_group_name)
@@ -6,6 +6,6 @@ module Kitchen
6
6
  # driver and, with it, the whole Azure SDK.
7
7
  #
8
8
  # @return [String]
9
- AZURERM_VERSION = "2.1.4".freeze
9
+ AZURERM_VERSION = "2.4.0".freeze
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-azurerm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.4
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stuart Preston
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-23 00:00:00.000000000 Z
11
+ date: 2026-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inifile