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 +4 -4
- data/lib/kitchen/driver/dokken.rb +86 -19
- data/lib/kitchen/driver/dokken_version.rb +1 -1
- data/lib/kitchen/helpers.rb +78 -12
- data/lib/kitchen/provisioner/dokken.rb +22 -2
- data/lib/kitchen/transport/dokken.rb +16 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0dffedb0556f642eb7b9011de7ce16d3002b7fc94fc4f99e1f0350a3e593865
|
|
4
|
+
data.tar.gz: 0542043deb31d066f65640e32faaf45f66c62d6c5e77b8efa569d30dbbfa0ecf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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.
|
data/lib/kitchen/helpers.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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" =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|