-
Notifications
You must be signed in to change notification settings - Fork 92
/
Copy pathdokken.rb
524 lines (454 loc) · 15.4 KB
/
dokken.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
#
# Author:: Sean OMeara (<[email protected]>)
#
# Copyright (C) 2015, Sean OMeara
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'digest'
require 'kitchen'
require 'tmpdir'
require 'docker'
require 'lockfile'
require_relative '../helpers'
include Dokken::Helpers
# FIXME: - make true
Excon.defaults[:ssl_verify_peer] = false
module Kitchen
module Driver
# Dokken driver for Kitchen.
#
# @author Sean OMeara <[email protected]>
class Dokken < Kitchen::Driver::Base
default_config :api_retries, 20
default_config :binds, []
default_config :cap_add, nil
default_config :cap_drop, nil
default_config :chef_image, 'chef/chef'
default_config :chef_version, 'latest'
default_config :data_image, 'dokken/kitchen-cache:latest'
default_config :dns, nil
default_config :dns_search, nil
default_config :docker_info, docker_info
default_config :ports, nil
default_config :docker_host_url, default_docker_host
default_config :hostname, 'dokken'
default_config :image_prefix, nil
default_config :links, nil
default_config :network_mode, 'dokken'
default_config :pid_one_command, 'sh -c "trap exit 0 SIGTERM; while :; do sleep 1; done"'
default_config :privileged, false
default_config :read_timeout, 3600
default_config :security_opt, nil
default_config :volumes, nil
default_config :write_timeout, 3600
# (see Base#create)
def create(state)
# image to config
pull_platform_image
# network
make_dokken_network
# chef
pull_chef_image
create_chef_container state
# data
dokken_create_sandbox
if remote_docker_host?
make_data_image
start_data_container state
end
# work image
build_work_image state
# runner
start_runner_container state
# misc
save_misc_state state
end
def destroy(_state)
if remote_docker_host?
stop_data_container
delete_data_container
end
stop_runner_container
delete_runner_container
delete_work_image
dokken_delete_sandbox
end
private
class PartialHash < Hash
def ==(other)
other.is_a?(Hash) && all? { |key, val| other.key?(key) && other[key] == val }
end
end
def api_retries
config[:api_retries]
end
def docker_connection
opts = ::Docker.options
opts[:read_timeout] = config[:read_timeout]
opts[:write_timeout] = config[:write_timeout]
@docker_connection ||= ::Docker::Connection.new(config[:docker_host_url], opts)
end
def delete_work_image
return unless ::Docker::Image.exist?(work_image, {}, docker_connection)
with_retries { @work_image = ::Docker::Image.get(work_image, {}, docker_connection) }
with_retries do
begin
with_retries { @work_image.remove(force: true) }
rescue ::Docker::Error::ConflictError
debug "driver - #{work_image} cannot be removed"
end
end
end
def build_work_image(state)
info('Building work image..')
return if ::Docker::Image.exist?(work_image, {}, docker_connection)
begin
@intermediate_image = ::Docker::Image.build(
work_image_dockerfile,
{
't' => work_image,
},
docker_connection
)
rescue
raise 'work_image build failed'
end
state[:work_image] = work_image
end
def work_image_dockerfile
from = "FROM #{platform_image}"
custom = ['RUN /bin/sh -c "echo Built with Test Kitchen"']
Array(config[:intermediate_instructions]).each { |c| custom << c }
[from, custom].join("\n")
end
def save_misc_state(state)
state[:platform_image] = platform_image
state[:instance_name] = instance_name
state[:instance_platform_name] = instance_platform_name
state[:image_prefix] = image_prefix
end
def delete_chef_container
debug "driver - deleting container #{chef_container_name}"
delete_container chef_container_name
end
def delete_data_container
debug "driver - deleting container #{data_container_name}"
delete_container data_container_name
end
def delete_runner_container
debug "driver - deleting container #{runner_container_name}"
delete_container runner_container_name
end
def image_prefix
config[:image_prefix]
end
def instance_platform_name
instance.platform.name
end
def stop_runner_container
debug "driver - stopping container #{runner_container_name}"
stop_container runner_container_name
end
def stop_data_container
debug "driver - stopping container #{data_container_name}"
stop_container data_container_name
end
def work_image
return "#{image_prefix}/#{instance_name}" unless image_prefix.nil?
instance_name
end
def dokken_binds
ret = []
ret << "#{dokken_kitchen_sandbox}:/opt/kitchen" unless dokken_kitchen_sandbox.nil? || remote_docker_host?
ret << "#{dokken_verifier_sandbox}:/opt/verifier" unless dokken_verifier_sandbox.nil? || remote_docker_host?
ret << Array(config[:binds]) unless config[:binds].nil?
ret.flatten
end
def dokken_volumes
coerce_volumes(config[:volumes])
end
def coerce_volumes(v)
case v
when PartialHash, nil
v
when Hash
PartialHash[v]
else
b = []
v = Array(v).to_a # in case v.is_A?(Chef::Node::ImmutableArray)
v.delete_if do |x|
parts = x.split(':')
b << x if parts.length > 1
end
b = nil if b.empty?
config[:binds].push(b) unless config[:binds].include?(b) || b.nil?
return PartialHash.new if v.empty?
v.each_with_object(PartialHash.new) { |volume, h| h[volume] = {} }
end
end
def dokken_volumes_from
ret = []
ret << chef_container_name
ret << data_container_name if remote_docker_host?
ret
end
def start_runner_container(state)
debug "driver - starting #{runner_container_name}"
config = {
'name' => runner_container_name,
'Cmd' => Shellwords.shellwords(self[:pid_one_command]),
'Image' => "#{repo(work_image)}:#{tag(work_image)}",
'Hostname' => self[:hostname],
'ExposedPorts' => exposed_ports,
'Volumes' => dokken_volumes,
'HostConfig' => {
'Privileged' => self[:privileged],
'VolumesFrom' => dokken_volumes_from,
'Binds' => dokken_binds,
'Dns' => self[:dns],
'DnsSearch' => self[:dns_search],
'Links' => Array(self[:links]),
'CapAdd' => Array(self[:cap_add]),
'CapDrop' => Array(self[:cap_drop]),
'SecurityOpt' => Array(self[:security_opt]),
'NetworkMode' => self[:network_mode],
'PortBindings' => port_bindings,
},
'NetworkingConfig' => {
'EndpointsConfig' => {
self[:network_mode] => {
'Aliases' => Array(self[:hostname]),
},
},
},
}
runner_container = run_container(config)
state[:runner_container] = runner_container.json
end
def start_data_container(state)
debug "driver - creating #{data_container_name}"
config = {
'name' => data_container_name,
'Image' => "#{repo(data_image)}:#{tag(data_image)}",
'HostConfig' => {
'PortBindings' => port_bindings,
'PublishAllPorts' => true,
'NetworkMode' => 'bridge',
},
'NetworkingConfig' => {
'EndpointsConfig' => {
self[:network_mode] => {
'Aliases' => Array(self[:hostname]),
},
},
},
}
data_container = run_container(config)
state[:data_container] = data_container.json
end
def make_dokken_network
lockfile = Lockfile.new "#{home_dir}/.dokken-network.lock"
begin
lockfile.lock
with_retries { ::Docker::Network.get('dokken', {}, docker_connection) }
rescue
begin
with_retries { ::Docker::Network.create('dokken', {}) }
rescue ::Docker::Error => e
debug "driver - error :#{e}:"
end
ensure
lockfile.unlock
end
end
def make_data_image
debug 'driver - calling create_data_image'
create_data_image
end
def create_chef_container(state)
lockfile = Lockfile.new "#{home_dir}/.dokken-#{chef_container_name}.lock"
begin
lockfile.lock
with_retries { ::Docker::Container.get(chef_container_name, {}, docker_connection) }
rescue ::Docker::Error::NotFoundError
with_retries do
begin
debug "driver - creating volume container #{chef_container_name} from #{chef_image}"
config = {
'name' => chef_container_name,
'Cmd' => 'true',
'Image' => "#{repo(chef_image)}:#{tag(chef_image)}",
'HostConfig' => {
'NetworkMode' => self[:network_mode],
},
}
chef_container = create_container(config)
state[:chef_container] = chef_container.json
rescue ::Docker::Error => e
raise "driver - #{chef_container_name} failed to create #{e}"
end
end
ensure
lockfile.unlock
end
end
def pull_platform_image
debug "driver - pulling #{chef_image} #{repo(platform_image)} #{tag(platform_image)}"
pull_if_missing platform_image
end
def pull_chef_image
debug "driver - pulling #{chef_image} #{repo(chef_image)} #{tag(chef_image)}"
pull_if_missing chef_image
end
def delete_image(name)
with_retries { @image = ::Docker::Image.get(name, {}, docker_connection) }
with_retries { @image.remove(force: true) }
rescue ::Docker::Error
puts "Image #{name} not found. Nothing to delete."
end
def container_exist?(name)
return true if ::Docker::Container.get(name, {}, docker_connection)
rescue
false
end
def parse_image_name(image)
parts = image.split(':')
if parts.size > 2
tag = parts.pop
repo = parts.join(':')
else
tag = parts[1] || 'latest'
repo = parts[0]
end
[repo, tag]
end
def repo(image)
parse_image_name(image)[0]
end
def create_container(args)
with_retries { @container = ::Docker::Container.get(args['name'], {}, docker_connection) }
rescue
with_retries do
begin
info "Creating container #{args['name']}"
debug "driver - create_container args #{args}"
with_retries do
begin
@container = ::Docker::Container.create(args.clone, docker_connection)
rescue ::Docker::Error::ConflictError
debug "driver - rescue ConflictError: #{args['name']}"
with_retries { @container = ::Docker::Container.get(args['name'], {}, docker_connection) }
end
end
rescue ::Docker::Error => e
debug "driver - error :#{e}:"
raise "driver - failed to create_container #{args['name']}"
end
end
end
def run_container(args)
create_container(args)
with_retries do
@container.start
@container = ::Docker::Container.get(args['name'], {}, docker_connection)
wait_running_state(args['name'], true)
end
@container
end
def container_state
@container ? @container.info['State'] : {}
end
def stop_container(name)
with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
with_retries do
@container.stop(force: false)
wait_running_state(name, false)
end
rescue ::Docker::Error::NotFoundError
debug "Container #{name} not found. Nothing to stop."
end
def delete_container(name)
with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
with_retries { @container.delete(force: true, v: true) }
rescue ::Docker::Error::NotFoundError
debug "Container #{name} not found. Nothing to delete."
end
def wait_running_state(name, v)
@container = ::Docker::Container.get(name, {}, docker_connection)
i = 0
tries = 20
until container_state['Running'] == v || container_state['FinishedAt'] != '0001-01-01T00:00:00Z'
i += 1
break if i == tries
sleep 0.1
@container = ::Docker::Container.get(name, {}, docker_connection)
end
end
def tag(image)
parse_image_name(image)[1]
end
def chef_container_name
"chef-#{chef_version}"
end
def chef_image
"#{config[:chef_image]}:#{chef_version}"
end
def chef_version
return 'latest' if config[:chef_version] == 'stable'
config[:chef_version]
end
def data_container_name
"#{instance_name}-data"
end
def data_image
config[:data_image]
end
def platform_image
config[:image] || platform_image_from_name
end
def platform_image_from_name
platform, release = instance.platform.name.split('-')
release ? [platform, release].join(':') : platform
end
def pull_if_missing(image)
return if ::Docker::Image.exist?("#{repo(image)}:#{tag(image)}", {}, docker_connection)
pull_image image
end
def pull_image(image)
with_retries do
::Docker::Image.create({ 'fromImage' => "#{repo(image)}:#{tag(image)}" }, nil, docker_connection)
end
end
def runner_container_name
instance_name.to_s
end
def with_retries
tries = api_retries
begin
yield
# Only catch errors that can be fixed with retries.
rescue ::Docker::Error::ServerError, # 404
::Docker::Error::UnexpectedResponseError, # 400
::Docker::Error::TimeoutError,
::Docker::Error::IOError,
::Docker::Error::NotFoundError => e
tries -= 1
sleep 0.1
retry if tries > 0
debug "tries: #{tries} error: #{e}"
raise e
end
end
end
end
end