kitchen-dokken 2.23.6 → 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: 2c7f5b5652cdde267df1ead2dad29ea1c7e3f642cec4b86f3c5ef4c13612a3f3
4
- data.tar.gz: 8b94c596b93899da42069d35faed37d4be565c9af5617839ee84f9e44fcc9a86
3
+ metadata.gz: a0dffedb0556f642eb7b9011de7ce16d3002b7fc94fc4f99e1f0350a3e593865
4
+ data.tar.gz: 0542043deb31d066f65640e32faaf45f66c62d6c5e77b8efa569d30dbbfa0ecf
5
5
  SHA512:
6
- metadata.gz: d48c19a4bdbed503978b68451cded18f2437d2aa0c048d8285344447c4a27503b48937eaf29d93d2fd429316f9c8db240942f09a3333616c939a5cd58555f26d
7
- data.tar.gz: 305e81615cc269fdfc685436bbd19b47de69c465006c8ac7b55b42eb8911e4dbfd3e2b36759417347e109c56418176dfa69b5f5136f33669ff2b3e5ffbed4f62
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.
@@ -805,7 +829,11 @@ module Kitchen
805
829
  with_retries { @image = ::Docker::Image.get(name, { "platform" => oci_platform(config[:platform]) }, docker_connection) }
806
830
  with_retries { @image.remove(force: true) }
807
831
  rescue ::Docker::Error::DockerError
808
- puts "Image #{name} not found. Nothing to delete."
832
+ # `debug`, not `puts`: this is routine during destroy, and writing
833
+ # straight to stdout puts it outside kitchen's logging entirely --
834
+ # it bypasses the log level, never reaches `.kitchen/logs`, and
835
+ # interleaves with kitchen's own output.
836
+ debug "Image #{name} not found. Nothing to delete."
809
837
  end
810
838
 
811
839
  # Whether the daemon knows about a container.
@@ -879,7 +907,7 @@ module Kitchen
879
907
  # @raise [RuntimeError] if the container could not be created
880
908
  def create_container(args, platform: nil)
881
909
  with_retries { @container = ::Docker::Container.get(args["name"], {}, docker_connection) }
882
- rescue
910
+ rescue ::Docker::Error::NotFoundError
883
911
  with_retries do
884
912
  # Merge rather than append: start_runner_container passes config[:env]
885
913
  # straight through, so mutating args["Env"] would edit the driver's
@@ -895,7 +923,11 @@ module Kitchen
895
923
  end
896
924
  rescue ::Docker::Error::DockerError => e
897
925
  debug "driver - error :#{e}:"
898
- 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)}"
899
931
  end
900
932
  end
901
933
 
@@ -939,15 +971,35 @@ module Kitchen
939
971
  # @raise [Kitchen::ActionFailed] if the container will not stay running
940
972
  def run_container(args, platform: nil)
941
973
  @container = create_container(args, platform: platform)
942
- with_retries do
943
- @container.start
944
- @container = ::Docker::Container.get(args["name"], {}, docker_connection)
945
- wait_running_state(args["name"], true)
946
- end
974
+ start_container!(args["name"])
947
975
  assert_running!(args["name"])
948
976
  @container
949
977
  end
950
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
+
951
1003
  # Fail the action when a container that has to run is not running.
952
1004
  #
953
1005
  # wait_running_state stops polling as soon as `FinishedAt` is set, which
@@ -966,10 +1018,25 @@ module Kitchen
966
1018
  def assert_running!(name)
967
1019
  return if container_state["Running"]
968
1020
 
969
- raise ActionFailed,
970
- "The #{name} container exited immediately after being started. " \
971
- "Its pid 1 did not stay up: check `pid_one_command`, `entrypoint`, " \
972
- "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)."
973
1040
  end
974
1041
 
975
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.6".freeze
21
+ DOKKEN_VERSION = "2.23.8".freeze
22
22
  end
23
23
  end
@@ -253,17 +253,30 @@ module Dokken
253
253
  "#{home_dir}/.dokken/verifier_sandbox/#{instance_name}"
254
254
  end
255
255
 
256
- # A container-safe, collision-free name for this kitchen instance.
256
+ # A container-safe, collision-free name for a kitchen instance.
257
257
  #
258
258
  # The working directory is hashed into the prefix so that the same suite
259
259
  # in two checkouts does not fight over one set of containers.
260
260
  #
261
+ # Defined on the module itself because Kitchen's Provisioner and Verifier
262
+ # base classes need the same answer but do not mix this module in -- they
263
+ # used to carry their own copies of these two lines, and three copies of
264
+ # a container-naming rule is three chances for them to drift apart.
265
+ #
266
+ # @param instance [Object] the kitchen instance
261
267
  # @return [String] the instance name
262
- def instance_name
268
+ def self.instance_name_for(instance)
263
269
  prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
264
270
  "#{prefix}-#{instance.name}".downcase
265
271
  end
266
272
 
273
+ # (see Dokken::Helpers.instance_name_for)
274
+ #
275
+ # @return [String] the instance name
276
+ def instance_name
277
+ ::Dokken::Helpers.instance_name_for(instance)
278
+ end
279
+
267
280
  # The `ExposedPorts` value for the runner container.
268
281
  #
269
282
  # @return [Hash, nil] a Docker API ExposedPorts hash
@@ -338,10 +351,15 @@ module Dokken
338
351
  # each optionally suffixed with `/protocol` and each allowing an
339
352
  # inclusive `low-high` container port range.
340
353
  #
341
- # @param v [String] a port specification
354
+ # @param v [String, Integer] a port specification
342
355
  # @return [Array<Hash>] one entry per container port
343
- # @raise [Kitchen::UserError] if a port range is inverted
356
+ # @raise [Kitchen::UserError] if a port range is inverted or unpairable
344
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
345
363
  parts = v.split(":")
346
364
  case parts.length
347
365
  when 3
@@ -377,18 +395,63 @@ module Dokken
377
395
  "Invalid port spec #{v.inspect}: no container port"
378
396
  end
379
397
 
380
- 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]
381
399
  # qualify the port-binding protocol even when it is implicitly tcp #427.
382
400
  protocol = "tcp" if protocol.nil?
383
- 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|
384
404
  {
385
405
  "host_ip" => host_ip,
386
- "host_port" => host_port,
406
+ "host_port" => host_ports[i],
387
407
  "container_port" => "#{port}/#{protocol}",
388
408
  }
389
409
  end
390
410
  end
391
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
+
392
455
  # Expand an inclusive `low-high` container port range.
393
456
  #
394
457
  # @param range [String] the range, e.g. "8080-8082"
@@ -453,7 +516,12 @@ module Dokken
453
516
  def running_inside_docker_desktop?
454
517
  Resolv.getaddress "host.docker.internal."
455
518
  true
456
- rescue
519
+ # Deliberately every StandardError, spelled out rather than bare. This is
520
+ # a probe: "can I resolve host.docker.internal" has exactly two useful
521
+ # answers, and a DNS timeout or a SocketError must be "no" rather than
522
+ # something that aborts a converge. Listing Resolv::ResolvError as well
523
+ # would be redundant -- it is a StandardError already.
524
+ rescue ::StandardError
457
525
  false
458
526
  end
459
527
 
@@ -521,8 +589,7 @@ module Kitchen
521
589
  # @return [String] the instance name
522
590
  # @see Dokken::Helpers#instance_name
523
591
  def instance_name
524
- prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
525
- "#{prefix}-#{instance.name}".downcase
592
+ ::Dokken::Helpers.instance_name_for(instance)
526
593
  end
527
594
  end
528
595
  end
@@ -557,8 +624,7 @@ module Kitchen
557
624
  # @return [String] the instance name
558
625
  # @see Dokken::Helpers#instance_name
559
626
  def instance_name
560
- prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
561
- "#{prefix}-#{instance.name}".downcase
627
+ ::Dokken::Helpers.instance_name_for(instance)
562
628
  end
563
629
 
564
630
  # Run the verifier against the instance.
@@ -79,7 +79,17 @@ module Kitchen
79
79
  create_sandbox
80
80
  write_run_command(run_command)
81
81
  instance.transport.connection(state) do |conn|
82
- if remote_docker_host? || running_inside_docker?
82
+ # Gate on what the driver recorded, not on a fresh probe. The driver
83
+ # already answered this at create time -- it built a data container
84
+ # or it did not -- and re-deriving the answer here means the two can
85
+ # disagree. Point DOCKER_HOST somewhere else between `kitchen
86
+ # create` and `kitchen converge` and they do: this side decides to
87
+ # upload, the container it would upload through was never created,
88
+ # and the transport dies on nil several frames down.
89
+ #
90
+ # Kitchen::Verifier::Base#call has always gated on the recorded
91
+ # state; this is the provisioner agreeing with it.
92
+ unless state[:data_container].nil?
83
93
  info("Transferring files to #{instance.to_str}")
84
94
  conn.upload(sandbox_dirs, config[:root_path])
85
95
  end
@@ -156,6 +166,16 @@ module Kitchen
156
166
  # just use the defaults
157
167
  config[:chef_log_level] = "warn" if config[:chef_log_level].empty?
158
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]
159
179
  end
160
180
 
161
181
  private
@@ -204,7 +224,7 @@ module Kitchen
204
224
  # @return [String] the container name
205
225
  # @api private
206
226
  def runner_container_name
207
- instance.name.to_s
227
+ instance_name.to_s
208
228
  end
209
229
 
210
230
  # Empty the sandbox after a converge, keeping the directory itself.
@@ -121,6 +121,17 @@ module Kitchen
121
121
  # @raise [Kitchen::UserError] if docker_host_url is not tcp:// or unix://
122
122
  # @raise [Kitchen::Transport::TransportFailed] if the copy fails
123
123
  def upload(locals, remote)
124
+ # Every route through ssh_endpoint reads the data container out of
125
+ # state, so being handed none is not a transfer that fails -- it is
126
+ # a caller that should not have asked. Said plainly here rather than
127
+ # as `undefined method '[]' for nil` from three frames down.
128
+ if options[:data_container].nil?
129
+ raise Kitchen::Transport::TransportFailed,
130
+ "Cannot upload to #{options[:instance_name]}: no data container was created " \
131
+ "for this instance. Files are only shipped over ssh when the docker daemon " \
132
+ "cannot read the local filesystem; otherwise the sandbox is bind-mounted."
133
+ end
134
+
124
135
  ssh_ip, ssh_port = ssh_endpoint
125
136
 
126
137
  debug "ssh_ip : #{ssh_ip}"
@@ -477,7 +488,11 @@ module Kitchen
477
488
  # @return [TrueClass,FalseClass]
478
489
  def docker_for_mac_or_win?
479
490
  ::Docker.info(::Docker::Connection.new(config[:docker_host_url], {}))["Name"] == "docker-desktop"
480
- rescue
491
+ # `::StandardError`, spelled out. This method lives inside module
492
+ # Kitchen, where a bare `StandardError` resolves to
493
+ # Kitchen::StandardError -- which Excon::Error::Socket is not, so an
494
+ # unreachable daemon would escape instead of answering "no".
495
+ rescue ::StandardError
481
496
  false
482
497
  end
483
498
 
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.6
4
+ version: 2.23.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean OMeara