kitchen-dokken 2.23.7 → 2.24.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: c9cac8cad07f229a018567a935e33aaa71edaa04c65dd05a38afbcd10bc839e4
4
- data.tar.gz: 971a897b24f4a00f224d995f18ae7177712110ed4048d2a9d02fb03fcd8948a4
3
+ metadata.gz: 1601bf73b8bb5607d0a279d1d06e3cc5779b581616159b320ee6737210b9bd1d
4
+ data.tar.gz: 7e92f8c54e09f5746905496fa9388b2fe49f0ec8d7a33f63ac461a0f53c07618
5
5
  SHA512:
6
- metadata.gz: 578aca5a5d479624c15313d7d6695d8ea42f75e5822ed55f3c083bb9b561b5b0e1378fa0e27964b0444cfd93f1db63eb5d6fc3b124ac977683b792735bfae7d2
7
- data.tar.gz: 2bf35be1a50b68a4de9ac5e0d8ac674904165a65f730b472b30c488b9c786890d740586fc7189bf21f22d95fe49cb1bcb2be33456ae37bb43b25d144f3c02854
6
+ metadata.gz: f6fee7ac1a7586664f3e67047ad994a22c059f6782c99e737a1792dd3758c778f1c8bd3c6f79f6fe1406725dad6f3c1b8d297de1fb9630c4e326d67b23de5d2c
7
+ data.tar.gz: 1604d52bffac5be374ac62f2a509b584ff43303a5b6c98aed2ed0c1fdcdaa8ec9a8fc8ec17c65a8c56679574ff878452e6ee587ca5bf28e12e8a005f6d845d36
@@ -19,5 +19,5 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_dependency "docker-api", ">= 1.33", "< 3"
21
21
  spec.add_dependency "kitchen-omnibus-chef", ">= 1.0"
22
- spec.add_dependency "test-kitchen", ">= 1.15", "< 5"
22
+ spec.add_dependency "test-kitchen", ">= 3.0", "< 5"
23
23
  end
@@ -23,6 +23,7 @@ require "docker"
23
23
  require "shellwords" unless defined?(Shellwords)
24
24
  require "base64" unless defined?(Base64)
25
25
  require_relative "../helpers"
26
+ require_relative "dokken_version"
26
27
 
27
28
  include Dokken::Helpers
28
29
 
@@ -42,6 +43,14 @@ module Kitchen
42
43
  #
43
44
  # @author Sean OMeara <sean@sean.io>
44
45
  class Dokken < Kitchen::Driver::Base
46
+ kitchen_driver_api_version 2
47
+
48
+ # kitchen-dokken's own version, not test-kitchen's. `kitchen diagnose`
49
+ # is the first thing a bug report carries, and it used to report the
50
+ # test-kitchen version here -- which says nothing about the plugin the
51
+ # report is actually about.
52
+ plugin_version Kitchen::Driver::DOKKEN_VERSION
53
+
45
54
  default_config :api_retries, 20
46
55
  default_config :binds, []
47
56
  default_config :cap_add, nil
@@ -143,8 +152,65 @@ module Kitchen
143
152
  dokken_delete_sandbox
144
153
  end
145
154
 
155
+ # (see Base#status)
156
+ #
157
+ # `kitchen list --live` asks the driver whether the instance is really
158
+ # there rather than trusting the last recorded action, which is exactly
159
+ # the question a docker daemon can answer precisely. Without this the
160
+ # driver inherited Base's "unknown", so a container that was plainly
161
+ # `Up` still listed as `unknown`.
162
+ #
163
+ # The runner *is* the instance. The chef container is a shared volume
164
+ # and the data container only exists on the remote-daemon path, so
165
+ # neither says anything about whether this instance is up.
166
+ #
167
+ # Deliberately not wrapped in {#with_retries}: this backs a listing, and
168
+ # a slow answer is worse than an honest "unknown".
169
+ #
170
+ # @param _state [Hash] mutable instance state
171
+ # @return [Hash] normalized status data
172
+ def status(_state)
173
+ container_status(::Docker::Container.get(runner_container_name, {}, docker_connection))
174
+ rescue ::Docker::Error::NotFoundError
175
+ { live: false, state: "not created", source: "driver" }
176
+ # Every other failure -- an unreachable daemon most of all -- is an
177
+ # unknown rather than an exception. Kitchen would catch it either way,
178
+ # but it would then report the exception class instead of the daemon
179
+ # url that could not be reached.
180
+ rescue ::StandardError => e
181
+ {
182
+ live: nil,
183
+ state: "unknown",
184
+ source: "driver",
185
+ message: "could not ask the docker daemon at #{config[:docker_host_url]}: #{e.message}",
186
+ }
187
+ end
188
+
146
189
  private
147
190
 
191
+ # Translate a container's `State` into kitchen's status shape.
192
+ #
193
+ # @param container [::Docker::Container] the runner container
194
+ # @return [Hash] normalized status data
195
+ def container_status(container)
196
+ state = container.info["State"] || {}
197
+ running = state["Running"] == true
198
+
199
+ status = {
200
+ # Docker's own vocabulary -- created, running, paused, exited,
201
+ # restarting, dead -- says more than a boolean, and `kitchen list`
202
+ # prints it verbatim.
203
+ live: running,
204
+ state: state["Status"] || (running ? "running" : "stopped"),
205
+ source: "driver",
206
+ resource_id: container.info["Id"],
207
+ }
208
+
209
+ error = state["Error"].to_s
210
+ status[:message] = error unless error.empty?
211
+ status
212
+ end
213
+
148
214
  # Attach DNS settings to a network endpoint configuration, in place.
149
215
  #
150
216
  # @param endpoint_config [Hash] the EndpointsConfig entry to extend
@@ -240,17 +306,41 @@ module Kitchen
240
306
 
241
307
  # Pull the human-readable reason out of a failed build response.
242
308
  #
243
- # The daemon usually answers with a JSON document carrying an "error"
244
- # key, but a proxy or a plain-text 500 does not -- and a JSON::ParserError
245
- # raised from inside the rescue clause would bury the real failure.
246
- #
247
309
  # @param error [Exception] the error docker-api raised
248
310
  # @return [String] the daemon's explanation, or the raw response
249
311
  def build_error_detail(error)
250
- last_line = error.to_s.split("\r\n").last.to_s
251
- JSON.parse(last_line)["error"].to_s
312
+ daemon_message(error.to_s.split("\r\n").last.to_s)
313
+ end
314
+
315
+ # Pull the human-readable reason out of a docker-api error.
316
+ #
317
+ # docker-api raises with the daemon's response body as the message, so
318
+ # what reaches a rescue clause is a JSON document rather than a sentence:
319
+ # `{"message":"driver failed programming external connectivity ..."}`.
320
+ # Reporting that verbatim is barely better than reporting nothing.
321
+ #
322
+ # @param error [Exception] the error docker-api raised
323
+ # @return [String] the daemon's explanation, or the raw response
324
+ def docker_error_detail(error)
325
+ daemon_message(error.to_s)
326
+ end
327
+
328
+ # The explanation carried in a docker daemon response body.
329
+ #
330
+ # Most endpoints answer with a "message" key and the /build stream with
331
+ # an "error" one. A proxy or a plain-text 500 answers with neither -- and
332
+ # a JSON::ParserError raised from inside a rescue clause would bury the
333
+ # very failure it was called on to explain -- so anything unparseable
334
+ # comes back unchanged.
335
+ #
336
+ # @param body [String] a response body
337
+ # @return [String] the explanation, or the body unchanged
338
+ def daemon_message(body)
339
+ parsed = JSON.parse(body)
340
+ detail = parsed["message"] || parsed["error"]
341
+ detail.nil? ? body : detail.to_s
252
342
  rescue JSON::ParserError, TypeError
253
- last_line
343
+ body
254
344
  end
255
345
 
256
346
  # The Dockerfile for the work image.
@@ -899,7 +989,11 @@ module Kitchen
899
989
  end
900
990
  rescue ::Docker::Error::DockerError => e
901
991
  debug "driver - error :#{e}:"
902
- raise "driver - failed to create_container #{args["name"]}"
992
+ # The daemon's reason used to be dropped here, leaving `kitchen
993
+ # create` to report a bare "failed to create_container <name>" with
994
+ # nothing to act on -- the explanation existed, but only for someone
995
+ # who already knew to re-run at `-l debug`.
996
+ raise "driver - failed to create_container #{args["name"]}: #{docker_error_detail(e)}"
903
997
  end
904
998
  end
905
999
 
@@ -943,15 +1037,35 @@ module Kitchen
943
1037
  # @raise [Kitchen::ActionFailed] if the container will not stay running
944
1038
  def run_container(args, platform: nil)
945
1039
  @container = create_container(args, platform: platform)
946
- with_retries do
947
- @container.start
948
- @container = ::Docker::Container.get(args["name"], {}, docker_connection)
949
- wait_running_state(args["name"], true)
950
- end
1040
+ start_container!(args["name"])
951
1041
  assert_running!(args["name"])
952
1042
  @container
953
1043
  end
954
1044
 
1045
+ # Start a container and wait for it to be running.
1046
+ #
1047
+ # `start!`, not `start`: docker-api defines the unsuffixed form as "the
1048
+ # same, but rescue from ServerErrors", so every reason the daemon
1049
+ # refused -- a port already bound, an invalid mount, a capability the
1050
+ # kernel will not grant -- was swallowed on the way past. `with_retries`
1051
+ # then had nothing to retry and `assert_running!` was left to report a
1052
+ # container that "exited immediately" when in truth it had never
1053
+ # started, sending the user off to read `docker logs` output that does
1054
+ # not exist.
1055
+ #
1056
+ # @param name [String] the container name
1057
+ # @return [void]
1058
+ # @raise [Kitchen::ActionFailed] if the daemon would not start it
1059
+ def start_container!(name)
1060
+ with_retries do
1061
+ @container.start!
1062
+ @container = ::Docker::Container.get(name, {}, docker_connection)
1063
+ wait_running_state(name, true)
1064
+ end
1065
+ rescue ::Docker::Error::DockerError => e
1066
+ raise ActionFailed, "The #{name} container could not be started: #{docker_error_detail(e)}"
1067
+ end
1068
+
955
1069
  # Fail the action when a container that has to run is not running.
956
1070
  #
957
1071
  # wait_running_state stops polling as soon as `FinishedAt` is set, which
@@ -970,10 +1084,25 @@ module Kitchen
970
1084
  def assert_running!(name)
971
1085
  return if container_state["Running"]
972
1086
 
973
- raise ActionFailed,
974
- "The #{name} container exited immediately after being started. " \
975
- "Its pid 1 did not stay up: check `pid_one_command`, `entrypoint`, " \
976
- "and that the image can boot (`docker logs #{name}` shows why)."
1087
+ raise ActionFailed, "The #{name} container is not running. #{not_running_reason(name)}"
1088
+ end
1089
+
1090
+ # Why a container that should be running is not.
1091
+ #
1092
+ # The daemon records its own refusal in `State.Error` and leaves
1093
+ # `FinishedAt` unset, which is a different failure from a pid 1 that
1094
+ # started and exited -- and pointing at `docker logs` for the first kind
1095
+ # sends the user to an empty file.
1096
+ #
1097
+ # @param name [String] the container name
1098
+ # @return [String] a sentence naming the likely cause
1099
+ def not_running_reason(name)
1100
+ daemon_error = container_state["Error"].to_s
1101
+ return "The docker daemon refused to start it: #{daemon_error}" unless daemon_error.empty?
1102
+
1103
+ "Its pid 1 exited immediately (exit code #{container_state["ExitCode"]}): " \
1104
+ "check `pid_one_command`, `entrypoint`, and that the image can boot " \
1105
+ "(`docker logs #{name}` shows why)."
977
1106
  end
978
1107
 
979
1108
  # The current container's `State` payload.
@@ -18,6 +18,6 @@
18
18
  module Kitchen
19
19
  module Driver
20
20
  # Version string for Dokken Kitchen driver
21
- DOKKEN_VERSION = "2.23.7".freeze
21
+ DOKKEN_VERSION = "2.24.0".freeze
22
22
  end
23
23
  end
@@ -351,10 +351,15 @@ module Dokken
351
351
  # each optionally suffixed with `/protocol` and each allowing an
352
352
  # inclusive `low-high` container port range.
353
353
  #
354
- # @param v [String] a port specification
354
+ # @param v [String, Integer] a port specification
355
355
  # @return [Array<Hash>] one entry per container port
356
- # @raise [Kitchen::UserError] if a port range is inverted
356
+ # @raise [Kitchen::UserError] if a port range is inverted or unpairable
357
357
  def parse_port(v)
358
+ # `ports: [8080]` is a perfectly natural thing to write, and YAML hands
359
+ # it over as an Integer. That used to reach String#split and abort the
360
+ # create with `undefined method 'split' for an instance of Integer`,
361
+ # which names a type rather than the line of kitchen.yml at fault.
362
+ v = v.to_s
358
363
  parts = v.split(":")
359
364
  case parts.length
360
365
  when 3
@@ -390,18 +395,63 @@ module Dokken
390
395
  "Invalid port spec #{v.inspect}: no container port"
391
396
  end
392
397
 
393
- port_range = expand_port_range(port_range, v) if port_range.include?("-")
398
+ container_ports = port_range.include?("-") ? expand_port_range(port_range, v) : [port_range]
394
399
  # qualify the port-binding protocol even when it is implicitly tcp #427.
395
400
  protocol = "tcp" if protocol.nil?
396
- Array(port_range).map do |port|
401
+ host_ports = expand_host_ports(host_port, container_ports.length, v)
402
+
403
+ container_ports.each_with_index.map do |port, i|
397
404
  {
398
405
  "host_ip" => host_ip,
399
- "host_port" => host_port,
406
+ "host_port" => host_ports[i],
400
407
  "container_port" => "#{port}/#{protocol}",
401
408
  }
402
409
  end
403
410
  end
404
411
 
412
+ # Work out the host port that pairs with each container port.
413
+ #
414
+ # Docker pairs ranges off one for one -- `8080-8082:9090-9092` publishes
415
+ # 9090 on 8080, 9091 on 8081 and 9092 on 8082 -- but only the container
416
+ # side used to be expanded here, so every binding was handed the whole
417
+ # host range as its `HostPort`. That reads as "any free port in this
418
+ # range" to the daemon, which meant the mapping was only correct while the
419
+ # whole range happened to be free, and otherwise came out shifted or
420
+ # failed outright with "all ports are allocated" rather than naming the
421
+ # port that was taken.
422
+ #
423
+ # @param host_port [String] the host half of the spec
424
+ # @param count [Integer] how many container ports were asked for
425
+ # @param spec [String] the whole port spec, for the error message
426
+ # @return [Array<String>] one host port per container port
427
+ # @raise [Kitchen::UserError] if the two sides cannot be paired
428
+ def expand_host_ports(host_port, count, spec)
429
+ # `ports: '8080'` names no host port at all; the daemon picks one.
430
+ return Array.new(count, host_port) if host_port.empty?
431
+
432
+ unless host_port.include?("-")
433
+ return [host_port] if count == 1
434
+
435
+ raise Kitchen::UserError,
436
+ "Invalid port spec #{spec.inspect}: a single host port cannot be paired with " \
437
+ "a range of #{count} container ports. Give a host range of the same size, " \
438
+ "as in 8080-8082:9090-9092."
439
+ end
440
+
441
+ # A host range against a single container port is docker's "publish this
442
+ # on any free port in the range". The daemon does the choosing, so the
443
+ # range is handed over untouched.
444
+ return [host_port] if count == 1
445
+
446
+ host_ports = expand_port_range(host_port, spec)
447
+ return host_ports.map(&:to_s) if host_ports.length == count
448
+
449
+ raise Kitchen::UserError,
450
+ "Invalid port spec #{spec.inspect}: the host range covers #{host_ports.length} " \
451
+ "ports and the container range covers #{count}. Docker pairs them off one for " \
452
+ "one, so both ranges have to be the same size."
453
+ end
454
+
405
455
  # Expand an inclusive `low-high` container port range.
406
456
  #
407
457
  # @param range [String] the range, e.g. "8080-8082"
@@ -579,8 +629,39 @@ module Kitchen
579
629
 
580
630
  # Run the verifier against the instance.
581
631
  #
582
- # Files are only uploaded when the driver built a data container, which
583
- # it does exactly when the daemon cannot read the host filesystem.
632
+ # This replaces Kitchen::Verifier::Base#call outright, so it has to be
633
+ # read against the upstream version it stands in for. It diverges in
634
+ # three places, and each divergence is deliberate:
635
+ #
636
+ # 1. *The upload is gated on the data container.* Files are only shipped
637
+ # when the driver built one, which it does exactly when the daemon
638
+ # cannot read the host filesystem. Otherwise the sandboxes are
639
+ # bind-mounted and are already in place. `init_command` is gated with
640
+ # it because for Busser it clears the verifier root, which in
641
+ # bind-mount mode is the host sandbox that was just populated.
642
+ #
643
+ # 2. *`cleanup_sandbox` is not called*, and must not be. Upstream can
644
+ # `rmtree` its sandbox because a stock one is a throwaway
645
+ # `Dir.mktmpdir`. Dokken's is a stable per-instance directory that
646
+ # the driver bind-mounts into a long-lived container, and a bind
647
+ # mount is bound to an inode rather than to a path: removing the
648
+ # directory severs it permanently, and the `mkdir_p` in the next
649
+ # `create_sandbox` does not restore it. Verified against Docker --
650
+ # after an rmtree and a recreate, the host has the file and the
651
+ # container cannot see it, until the container itself is replaced.
652
+ # Kitchen::Provisioner::Dokken#cleanup_dokken_sandbox is the shape
653
+ # that is safe here: empty the directory, keep the inode.
654
+ #
655
+ # 3. *`config[:downloads]` is not honoured.* Upstream downloads files
656
+ # off the instance after the run command. That cannot work here yet
657
+ # for a second reason as well: Transport::Dokken::Connection does not
658
+ # implement `download`, so the base class would raise. Wiring both up
659
+ # is worth doing, and is the one divergence here that is a gap rather
660
+ # than a decision.
661
+ #
662
+ # spec/kitchen/verifier_override_contract_spec.rb pins the upstream
663
+ # facts this reasoning rests on, so that a test-kitchen release which
664
+ # invalidates any of them fails loudly rather than silently.
584
665
  #
585
666
  # @param state [Hash] mutable instance state
586
667
  # @return [void]
@@ -18,6 +18,7 @@
18
18
  require "kitchen"
19
19
  require "kitchen/provisioner/chef_infra"
20
20
  require_relative "../helpers"
21
+ require_relative "../driver/dokken_version"
21
22
 
22
23
  include Dokken::Helpers
23
24
 
@@ -35,7 +36,10 @@ module Kitchen
35
36
  class Dokken < Kitchen::Provisioner::ChefInfra
36
37
  kitchen_provisioner_api_version 2
37
38
 
38
- plugin_version Kitchen::VERSION
39
+ # kitchen-dokken's own version. Kitchen::VERSION is test-kitchen's,
40
+ # so `kitchen diagnose` reported the framework version as the
41
+ # plugin version for every dokken plugin.
42
+ plugin_version Kitchen::Driver::DOKKEN_VERSION
39
43
 
40
44
  default_config :root_path, "/opt/kitchen"
41
45
  default_config :chef_binary do |provisioner|
@@ -166,6 +170,16 @@ module Kitchen
166
170
  # just use the defaults
167
171
  config[:chef_log_level] = "warn" if config[:chef_log_level].empty?
168
172
  config[:chef_output_format] = "doc" if config[:chef_output_format].empty?
173
+
174
+ # ChefBase#chef_args appends `--log_level #{config[:log_level]}` to
175
+ # whatever base command it is handed, so the parent's flag lands
176
+ # *after* the `-l` built below. `-l` and `--log_level` are the same
177
+ # chef-client option and the last occurrence wins, which meant
178
+ # chef_log_level was parsed, written into the staged run_command, and
179
+ # then silently overridden by the parent's default of "auto".
180
+ # Keeping the two in step makes the documented setting the one chef
181
+ # actually runs at.
182
+ config[:log_level] = config[:chef_log_level]
169
183
  end
170
184
 
171
185
  private
@@ -22,6 +22,7 @@ require "digest/sha1" unless defined?(Digest::SHA1)
22
22
  require "open3" unless defined?(Open3)
23
23
  require "shellwords" unless defined?(Shellwords)
24
24
  require_relative "../helpers"
25
+ require_relative "../driver/dokken_version"
25
26
 
26
27
  include Dokken::Helpers
27
28
 
@@ -45,7 +46,10 @@ module Kitchen
45
46
  class Dokken < Kitchen::Transport::Base
46
47
  kitchen_transport_api_version 2
47
48
 
48
- plugin_version Kitchen::VERSION
49
+ # kitchen-dokken's own version. Kitchen::VERSION is test-kitchen's,
50
+ # so `kitchen diagnose` reported the framework version as the
51
+ # plugin version for every dokken plugin.
52
+ plugin_version Kitchen::Driver::DOKKEN_VERSION
49
53
 
50
54
  default_config :docker_info do |transport|
51
55
  docker_info(transport[:docker_host_url])
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-dokken
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.23.7
4
+ version: 2.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean OMeara
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '1.15'
53
+ version: '3.0'
54
54
  - - "<"
55
55
  - !ruby/object:Gem::Version
56
56
  version: '5'
@@ -60,7 +60,7 @@ dependencies:
60
60
  requirements:
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
- version: '1.15'
63
+ version: '3.0'
64
64
  - - "<"
65
65
  - !ruby/object:Gem::Version
66
66
  version: '5'