kitchen-dokken 2.23.7 → 2.23.8

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: a0dffedb0556f642eb7b9011de7ce16d3002b7fc94fc4f99e1f0350a3e593865
4
+ data.tar.gz: 0542043deb31d066f65640e32faaf45f66c62d6c5e77b8efa569d30dbbfa0ecf
5
5
  SHA512:
6
- metadata.gz: 578aca5a5d479624c15313d7d6695d8ea42f75e5822ed55f3c083bb9b561b5b0e1378fa0e27964b0444cfd93f1db63eb5d6fc3b124ac977683b792735bfae7d2
7
- data.tar.gz: 2bf35be1a50b68a4de9ac5e0d8ac674904165a65f730b472b30c488b9c786890d740586fc7189bf21f22d95fe49cb1bcb2be33456ae37bb43b25d144f3c02854
6
+ metadata.gz: f0717edbd835bdba7ba34d86f051b19427506871cde23003d7cd11c9ccf2fdc32a876d02b32c66fb0b370382648863053b671046d921e04318d5ebf13af0a580
7
+ data.tar.gz: 7d99975e0a3532d9c62acd42cf84cc258514bb591621d63de40f53f1b814b090cc8d7eac777b7fbd179d7a6b81e1a38ae461e957db6a551bb91fd7a53c98491a
@@ -240,17 +240,41 @@ module Kitchen
240
240
 
241
241
  # Pull the human-readable reason out of a failed build response.
242
242
  #
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
243
  # @param error [Exception] the error docker-api raised
248
244
  # @return [String] the daemon's explanation, or the raw response
249
245
  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
246
+ daemon_message(error.to_s.split("\r\n").last.to_s)
247
+ end
248
+
249
+ # Pull the human-readable reason out of a docker-api error.
250
+ #
251
+ # docker-api raises with the daemon's response body as the message, so
252
+ # what reaches a rescue clause is a JSON document rather than a sentence:
253
+ # `{"message":"driver failed programming external connectivity ..."}`.
254
+ # Reporting that verbatim is barely better than reporting nothing.
255
+ #
256
+ # @param error [Exception] the error docker-api raised
257
+ # @return [String] the daemon's explanation, or the raw response
258
+ def docker_error_detail(error)
259
+ daemon_message(error.to_s)
260
+ end
261
+
262
+ # The explanation carried in a docker daemon response body.
263
+ #
264
+ # Most endpoints answer with a "message" key and the /build stream with
265
+ # an "error" one. A proxy or a plain-text 500 answers with neither -- and
266
+ # a JSON::ParserError raised from inside a rescue clause would bury the
267
+ # very failure it was called on to explain -- so anything unparseable
268
+ # comes back unchanged.
269
+ #
270
+ # @param body [String] a response body
271
+ # @return [String] the explanation, or the body unchanged
272
+ def daemon_message(body)
273
+ parsed = JSON.parse(body)
274
+ detail = parsed["message"] || parsed["error"]
275
+ detail.nil? ? body : detail.to_s
252
276
  rescue JSON::ParserError, TypeError
253
- last_line
277
+ body
254
278
  end
255
279
 
256
280
  # The Dockerfile for the work image.
@@ -899,7 +923,11 @@ module Kitchen
899
923
  end
900
924
  rescue ::Docker::Error::DockerError => e
901
925
  debug "driver - error :#{e}:"
902
- raise "driver - failed to create_container #{args["name"]}"
926
+ # The daemon's reason used to be dropped here, leaving `kitchen
927
+ # create` to report a bare "failed to create_container <name>" with
928
+ # nothing to act on -- the explanation existed, but only for someone
929
+ # who already knew to re-run at `-l debug`.
930
+ raise "driver - failed to create_container #{args["name"]}: #{docker_error_detail(e)}"
903
931
  end
904
932
  end
905
933
 
@@ -943,15 +971,35 @@ module Kitchen
943
971
  # @raise [Kitchen::ActionFailed] if the container will not stay running
944
972
  def run_container(args, platform: nil)
945
973
  @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
974
+ start_container!(args["name"])
951
975
  assert_running!(args["name"])
952
976
  @container
953
977
  end
954
978
 
979
+ # Start a container and wait for it to be running.
980
+ #
981
+ # `start!`, not `start`: docker-api defines the unsuffixed form as "the
982
+ # same, but rescue from ServerErrors", so every reason the daemon
983
+ # refused -- a port already bound, an invalid mount, a capability the
984
+ # kernel will not grant -- was swallowed on the way past. `with_retries`
985
+ # then had nothing to retry and `assert_running!` was left to report a
986
+ # container that "exited immediately" when in truth it had never
987
+ # started, sending the user off to read `docker logs` output that does
988
+ # not exist.
989
+ #
990
+ # @param name [String] the container name
991
+ # @return [void]
992
+ # @raise [Kitchen::ActionFailed] if the daemon would not start it
993
+ def start_container!(name)
994
+ with_retries do
995
+ @container.start!
996
+ @container = ::Docker::Container.get(name, {}, docker_connection)
997
+ wait_running_state(name, true)
998
+ end
999
+ rescue ::Docker::Error::DockerError => e
1000
+ raise ActionFailed, "The #{name} container could not be started: #{docker_error_detail(e)}"
1001
+ end
1002
+
955
1003
  # Fail the action when a container that has to run is not running.
956
1004
  #
957
1005
  # wait_running_state stops polling as soon as `FinishedAt` is set, which
@@ -970,10 +1018,25 @@ module Kitchen
970
1018
  def assert_running!(name)
971
1019
  return if container_state["Running"]
972
1020
 
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)."
1021
+ raise ActionFailed, "The #{name} container is not running. #{not_running_reason(name)}"
1022
+ end
1023
+
1024
+ # Why a container that should be running is not.
1025
+ #
1026
+ # The daemon records its own refusal in `State.Error` and leaves
1027
+ # `FinishedAt` unset, which is a different failure from a pid 1 that
1028
+ # started and exited -- and pointing at `docker logs` for the first kind
1029
+ # sends the user to an empty file.
1030
+ #
1031
+ # @param name [String] the container name
1032
+ # @return [String] a sentence naming the likely cause
1033
+ def not_running_reason(name)
1034
+ daemon_error = container_state["Error"].to_s
1035
+ return "The docker daemon refused to start it: #{daemon_error}" unless daemon_error.empty?
1036
+
1037
+ "Its pid 1 exited immediately (exit code #{container_state["ExitCode"]}): " \
1038
+ "check `pid_one_command`, `entrypoint`, and that the image can boot " \
1039
+ "(`docker logs #{name}` shows why)."
977
1040
  end
978
1041
 
979
1042
  # 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.23.8".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"
@@ -166,6 +166,16 @@ module Kitchen
166
166
  # just use the defaults
167
167
  config[:chef_log_level] = "warn" if config[:chef_log_level].empty?
168
168
  config[:chef_output_format] = "doc" if config[:chef_output_format].empty?
169
+
170
+ # ChefBase#chef_args appends `--log_level #{config[:log_level]}` to
171
+ # whatever base command it is handed, so the parent's flag lands
172
+ # *after* the `-l` built below. `-l` and `--log_level` are the same
173
+ # chef-client option and the last occurrence wins, which meant
174
+ # chef_log_level was parsed, written into the staged run_command, and
175
+ # then silently overridden by the parent's default of "auto".
176
+ # Keeping the two in step makes the documented setting the one chef
177
+ # actually runs at.
178
+ config[:log_level] = config[:chef_log_level]
169
179
  end
170
180
 
171
181
  private
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.23.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean OMeara