heighliner 0.9.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.
@@ -0,0 +1,714 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'heighliner/command_runner'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module Heighliner
7
+ # The commandline
8
+ class Cli
9
+ extend Heighliner::CliOptions
10
+
11
+ attr_reader :use_steerfile
12
+
13
+ def initialize
14
+ @use_steerfile = true
15
+ end
16
+
17
+ def set_config
18
+ # This is here for backwards compatibility since it can be used in Steerfiles.
19
+ # It would be a good idea to deprecate this and make it more abstract.
20
+ @work_dir = Config.work_dir
21
+ @config_dir = Config.config_dir
22
+ @config_file = Config.config_file
23
+ @steerfile = Config.steerfile
24
+ @config = Config.config
25
+ @out = Config.out
26
+ @info_out = Config.info_out
27
+
28
+ @steerfile.validate! if @use_steerfile
29
+ end
30
+
31
+ # At first I did this in the constructor but the problem with that is Optimist
32
+ # will parse the entire commandline for the first Cli command registered.
33
+ # That means no matter what you call -h or --help on, it will always return the help
34
+ # for the first subcommand. Fixed this by only running define_options when
35
+ # a command is run. We can't just run the constructor at that point because
36
+ # we need each Cli class to be constructed in the beginning so we can add their
37
+ # usage text to the output of `heighliner -h`.
38
+ def define_options(global_opts = [])
39
+ # We can't just call usage within the options block because that actually shifts
40
+ # the scope to Optimist::Parser. We can still reference variables but we can't
41
+ # call instance methods of a Heighliner::Cli class.
42
+ u = usage
43
+ Optimist.options do
44
+ banner u
45
+
46
+ global_opts.each { |o| opt(*o) }
47
+ end
48
+ end
49
+
50
+ def self.register(name, klass)
51
+ @subcommands ||= {}
52
+ @subcommands[name] = klass.new
53
+ end
54
+
55
+ def self.run_command(name, global_opts)
56
+ cmd = @subcommands[name]
57
+ opts = cmd.define_options(global_opts + cmd.class.options)
58
+
59
+ # The define_options method has stripped all arguments from the cli so now
60
+ # all that we're left with in ARGV are the subcommand to be run and possibly
61
+ # its own subcommands. We remove the subcommand here so each subcommand can
62
+ # easily use ARGV.shift to access its own subcommands.
63
+ ARGV.shift
64
+
65
+ Heighliner::Config.load(Dir.pwd, use_steerfile: cmd.use_steerfile)
66
+
67
+ # We do all this work in here instead of the exe/heighliner file because we
68
+ # want -h options to output before we check if a Steerfile exists.
69
+ # If we do it in exe/heighliner, people won't be able to check help messages
70
+ # unless they create a Steerfile first.
71
+ if opts[:quiet]
72
+ Config.out = File.open(File::NULL, 'w')
73
+ Config.info_out = File.open(File::NULL, 'w')
74
+ elsif opts[:verbose] || Config.always_verbose?
75
+ Config.out = $stderr
76
+ Config.info_out = Heighliner::AfterDotter.new(dotter: Heighliner::Dotter.new)
77
+ else
78
+ Config.out = Heighliner::Dotter.new
79
+ Config.info_out = Heighliner::AfterDotter.new(dotter: Config.out)
80
+ end
81
+
82
+ cmd.set_config
83
+
84
+ cmd.execute(opts)
85
+ end
86
+
87
+ def self.all_subcommands_usage
88
+ output = ''
89
+
90
+ @subcommands.each do |name, klass|
91
+ name_s = name.to_s
92
+
93
+ output += "#{name_s}\n"
94
+ output += name_s.gsub(/./, '-')
95
+ output += "\n"
96
+ output += klass.usage
97
+ output += "\n\n"
98
+ end
99
+
100
+ output
101
+ end
102
+
103
+ def stop_app
104
+ Config.info_out.puts 'Stopping application'
105
+ killrm app_container_name
106
+ stop_services
107
+ end
108
+
109
+ def start_services
110
+ services.each do |service|
111
+ Config.info_out.puts "Starting service: #{service.name}"
112
+ run_if_dead(
113
+ service.shared_name, service.start_docker_command
114
+ )
115
+ end
116
+ end
117
+
118
+ def stop_services
119
+ services.each do |service|
120
+ Config.info_out.puts "Stopping service: #{service.name}"
121
+ killrm service.shared_name
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def ensure_db_volume
128
+ create_if_volume_not_exist db_volume_name
129
+ end
130
+
131
+ def setup_db
132
+ ensure_db_volume
133
+ start_db
134
+ return if File.exist?(default_db_image)
135
+ return unless db_present?
136
+
137
+ # Some databases keep state around, best to clean it.
138
+ stop_db
139
+ delete_db_volume
140
+ start_db
141
+
142
+ Config.info_out.puts 'Provisioning database'
143
+ killrm "#{envname}-apptemp"
144
+ CommandRunner.run! Config.out, "docker run -ti
145
+ --rm
146
+ --name #{envname}-apptemp
147
+ --network #{Config.config[:networkname]}
148
+ #{app_params}
149
+ heighliner:#{envname}-#{current_branch} #{db_reset_command}"
150
+
151
+ save_db('default')
152
+ end
153
+
154
+ def save_db(name)
155
+ return unless db_present?
156
+
157
+ killrm db_container_name
158
+ save_db_state_from container: db_volume_name, to_file: db_image_path(name)
159
+ start_db
160
+ end
161
+
162
+ def load_db(name)
163
+ return unless db_present?
164
+
165
+ check_db_image_exists(name)
166
+ killrm db_container_name
167
+ CommandRunner.run Config.out, "docker volume rm #{db_volume_name}"
168
+ delete_db_volume
169
+ create_if_volume_not_exist db_volume_name
170
+ load_db_state_from file: db_image_path(name), to_container: db_volume_name
171
+ start_db
172
+ end
173
+
174
+ def check_db_image_exists(name)
175
+ return if File.exist?(db_image_path(name))
176
+
177
+ Optimist.die 'No saved state exists with that name'
178
+ end
179
+
180
+ def save_db_state_from(container:, to_file:)
181
+ Config.info_out.puts 'Saving database state'
182
+ File.write(to_file, '')
183
+ CommandRunner.run Config.out, "docker run --rm
184
+ -v #{container}:#{db_data_directory}
185
+ -v #{to_file}:#{to_file}
186
+ ruby:alpine
187
+ tar cvjf #{to_file} #{db_data_directory}"
188
+ end
189
+
190
+ def load_db_state_from(file:, to_container:)
191
+ Config.info_out.puts 'Loading database state'
192
+ CommandRunner.run Config.out, "docker run --rm
193
+ -v #{to_container}:#{db_data_directory}
194
+ -v #{file}:#{file}
195
+ ruby:alpine
196
+ tar xvjf #{file} -C #{db_data_directory}
197
+ --strip #{db_data_directory.scan(%r{/}).count}"
198
+ end
199
+
200
+ def stop_db
201
+ return unless db_present?
202
+
203
+ Config.info_out.puts 'Stopping database'
204
+ killrm db_container_name
205
+ end
206
+
207
+ def start_db
208
+ return unless db_present?
209
+
210
+ Config.info_out.puts 'Starting up database'
211
+ run_if_dead db_container_name, "docker run -d
212
+ -p #{db_port}:#{db_expose}
213
+ -v #{db_volume_name}:#{db_data_directory}
214
+ --name #{db_container_name}
215
+ --network #{network_name}
216
+ #{db_params}
217
+ #{db_image}
218
+ #{db_commands}"
219
+ wait_for_db unless db_waitscript.nil?
220
+ end
221
+
222
+ def delete_db_volume
223
+ CommandRunner.run Config.out, "docker volume rm #{db_volume_name}"
224
+ end
225
+
226
+ def current_branch_db_image_dir
227
+ "#{Config.config_dir}/databases/#{envname}/#{current_branch}"
228
+ end
229
+
230
+ def db_image_path(name)
231
+ if name.start_with?('./')
232
+ path = "#{home_dir_loc}/#{name.sub('./', '')}"
233
+ Config.info_out.puts "Database image path is: #{path}"
234
+ return path
235
+ end
236
+ FileUtils.mkdir_p current_branch_db_image_dir
237
+ "#{current_branch_db_image_dir}/#{name}.tar.bz"
238
+ end
239
+
240
+ def default_db_image
241
+ db_image_path('default')
242
+ end
243
+
244
+ def attach_app
245
+ start_services
246
+
247
+ puts 'Attaching to app...'
248
+
249
+ cmd = (ARGV || []).join(' ')
250
+ killrm app_container_name
251
+
252
+ attach_mounts = Config.steerfile.attach_mounts
253
+ volumes = attach_mounts.map { |from, to| "-v #{`pwd`.chomp}/#{from}:#{to}" }.join(' ')
254
+
255
+ cmd = "docker run -ti
256
+ --name #{app_container_name}
257
+ --network #{network_name}
258
+ --dns #{ip_of_container(Config.config[:shared_names][:dns])}
259
+ --dns-search #{http_suffix}
260
+ -p #{app_port}:#{app_expose}
261
+ -e DEV_APPLICATION_HOST=#{envname}.#{http_suffix}
262
+ -e VIRTUAL_HOST=#{envname}.#{http_suffix}
263
+ -e VIRTUAL_PORT=#{app_expose}
264
+ #{volumes}
265
+ #{app_params}
266
+ heighliner:#{envname}-#{current_branch} #{cmd}".tr("\n", ' ')
267
+
268
+ puts cmd
269
+ system cmd
270
+
271
+ stop_services
272
+
273
+ Config.out.puts 'Cleaning up...'
274
+ end
275
+
276
+ def start_app
277
+ start_services
278
+
279
+ Config.info_out.puts 'Starting up application'
280
+ killrm app_container_name
281
+ CommandRunner.run! Config.out, "docker run -d
282
+ --name #{app_container_name}
283
+ --network #{network_name}
284
+ --dns #{ip_of_container(Config.config[:shared_names][:dns])}
285
+ --dns-search #{http_suffix}
286
+ -p #{app_port}:#{app_expose}
287
+ -e DEV_APPLICATION_HOST=#{envname}.#{http_suffix}
288
+ -e VIRTUAL_HOST=#{envname}.#{http_suffix}
289
+ -e VIRTUAL_PORT=#{app_expose}
290
+ #{app_params}
291
+ heighliner:#{envname}-#{current_branch}"
292
+ wait_for_app
293
+ end
294
+
295
+ def tmp_waitscript_name
296
+ "#{Config.config_dir}/#{envname}-dbwaitscript"
297
+ end
298
+
299
+ def tmp_dockerfile_name
300
+ "#{Config.config_dir}/#{envname}-dockerfile"
301
+ end
302
+
303
+ def tmp_db_waiter
304
+ "#{envname}-dbwait"
305
+ end
306
+
307
+ def tmp_file_container
308
+ "#{envname}-tmpfiles"
309
+ end
310
+
311
+ def tmp_file_volume
312
+ "#{envname}-tmpfiles-vol"
313
+ end
314
+
315
+ def run_blocking_script(image, params, script, &block)
316
+ killrm tmp_db_waiter
317
+ killrm tmp_file_container
318
+
319
+ create_if_volume_not_exist tmp_file_volume
320
+
321
+ CommandRunner.run! Config.out, "docker create
322
+ -v #{tmp_file_volume}:/tmpvol
323
+ --name #{tmp_file_container} alpine"
324
+
325
+ File.write(tmp_waitscript_name, script)
326
+
327
+ CommandRunner.run! Config.out, "docker cp
328
+ #{tmp_waitscript_name}
329
+ #{tmp_file_container}:/tmpvol/wait.sh"
330
+
331
+ CommandRunner.run!(
332
+ Config.out,
333
+ "docker run --rm -ti
334
+ --name #{tmp_db_waiter}
335
+ --network #{network_name}
336
+ -v #{tmp_file_volume}:/tmpvol
337
+ #{params}
338
+ #{image} sh /tmpvol/wait.sh",
339
+ &block
340
+ )
341
+ ensure
342
+ killrm tmp_file_container
343
+ FileUtils.rm(tmp_waitscript_name)
344
+ end
345
+
346
+ def wait_for_app
347
+ return unless server_type == :http
348
+
349
+ Config.info_out.puts 'Waiting for server to start...'
350
+
351
+ http_code_extractor = "curl -s -o /dev/null -I -w \"\%<http_code>s\" http://#{app_container_name}:#{app_expose}"
352
+ unreachable_test = "#{http_code_extractor} | grep -q 000"
353
+
354
+ # This waitscript runs until curl returns a non-unreachable status code
355
+ # and then checks to see if its 200. If its not, it will raise an error.
356
+ wait_script = <<-SCRIPT
357
+ apk update
358
+ apk add curl
359
+ while #{unreachable_test}; do
360
+ echo 'o'
361
+ sleep 1
362
+ done
363
+ echo '#{http_code_extractor}'
364
+ echo $(#{http_code_extractor})
365
+ if [ "$(#{http_code_extractor})" != "200" ]; then
366
+ echo $(#{http_code_extractor})
367
+ else
368
+ echo '!'
369
+ fi
370
+ SCRIPT
371
+ run_blocking_script('alpine', '', wait_script) do |line|
372
+ # This script gets run every line that gets output.
373
+ # The '!' exclamation mark means success
374
+ # Three numbers means a status code has been returned
375
+ # If curl returns an error status the script will cut out and
376
+ # the app container died error will be displayed.
377
+ raise Heighliner::Error, "Failed with HTTP status: #{line}" if line =~ /^[0-9]{3}$/ && line != '200'
378
+
379
+ if line != '!' && container_dead?(app_container_name)
380
+ raise Heighliner::Error,
381
+ 'App container died. Run `heighliner logs` to see why.'
382
+ end
383
+ end
384
+
385
+ Config.info_out.puts 'Started successfully!'
386
+ end
387
+
388
+ def wait_for_db
389
+ return unless db_present?
390
+
391
+ Config.info_out.puts 'Waiting for database to start...'
392
+ run_blocking_script(db_image, db_waitscript_params, db_waitscript)
393
+ Config.info_out.puts 'Started.'
394
+ end
395
+
396
+ def network_name
397
+ Config.config[:networkname]
398
+ end
399
+
400
+ def services
401
+ @services ||= Config.steerfile.services.map { |name, info| Service.new(envname, name, info) }
402
+ end
403
+
404
+ def force_platform
405
+ Config.steerfile.platform || ''
406
+ end
407
+
408
+ def db_port
409
+ Config.config[:envs][envname][:db_port]
410
+ end
411
+
412
+ def db_expose
413
+ Config.steerfile.database[:port]
414
+ end
415
+
416
+ def db_params
417
+ eval_template Config.steerfile.database[:params]
418
+ end
419
+
420
+ def db_image
421
+ image = Config.steerfile.database[:image]
422
+ platform = Config.steerfile.database[:platform].presence
423
+ platform ? "--platform #{platform} #{image}" : image
424
+ end
425
+
426
+ def db_present?
427
+ db_image != 'none'
428
+ end
429
+
430
+ def db_commands
431
+ eval_template Config.steerfile.database[:commands]
432
+ end
433
+
434
+ def db_data_directory
435
+ Config.steerfile.database[:data_dir]
436
+ end
437
+
438
+ def server_type
439
+ Config.steerfile.server_type
440
+ end
441
+
442
+ def db_waitscript
443
+ eval_template Config.steerfile.database[:waitscript]
444
+ end
445
+
446
+ def db_waitscript_params
447
+ eval_template Config.steerfile.database[:waitscript_params]
448
+ end
449
+
450
+ def docker_file_contents
451
+ eval_template Config.steerfile.docker_file_contents
452
+ end
453
+
454
+ def docker_build_args
455
+ Config.steerfile.docker_build_args
456
+ end
457
+
458
+ def app_params
459
+ eval_template Config.steerfile.params
460
+ end
461
+
462
+ def db_reset_command
463
+ eval_template Config.steerfile.database_reset_command
464
+ end
465
+
466
+ def eval_template(value)
467
+ ERB.new(value).result(binding)
468
+ end
469
+
470
+ def app_port
471
+ Config.config[:envs][envname][:app_port]
472
+ end
473
+
474
+ def app_expose
475
+ Config.steerfile.port
476
+ end
477
+
478
+ def db_volume_name
479
+ "#{envname}-database"
480
+ end
481
+
482
+ def app_container_name
483
+ "#{envname}-app"
484
+ end
485
+
486
+ def db_container_name
487
+ "#{envname}-db"
488
+ end
489
+
490
+ def current_branch
491
+ `git branch | grep \\* | cut -d ' ' -f2`.chomp.gsub(/[^\-_0-9a-z]+/, '-')
492
+ end
493
+
494
+ def ensure_env
495
+ return unless envname.nil?
496
+
497
+ Optimist.die('No environment? Please use heighliner init <name>')
498
+ end
499
+
500
+ def http_suffix
501
+ Config.config[:http_suffix] || 'lvh.me'
502
+ end
503
+
504
+ def copy_keyfile(file)
505
+ Config.info_out.puts "Loading certificate file: #{file}"
506
+ if Config.config[:cert_source][:folder]
507
+ Config.info_out.puts " Source: folder (#{Config.config[:cert_source][:folder]})"
508
+ CommandRunner.run! Config.out, "docker run --rm
509
+ -v #{Config.config[:shared_names][:certs]}:/certs
510
+ -v #{Config.config[:cert_source][:folder]}:/cert_source
511
+ alpine cp /cert_source/#{file} /certs/#{file}"
512
+
513
+ elsif Config.config[:cert_source][:url]
514
+ Config.info_out.puts " Source: URL (#{Config.config[:cert_source][:url]}/#{file})"
515
+ CommandRunner.run! Config.out, "docker run --rm
516
+ -v #{Config.config[:shared_names][:certs]}:/certs
517
+ alpine wget #{Config.config[:cert_source][:url]}/#{file}
518
+ -O /certs/#{file}"
519
+
520
+ elsif Config.config[:cert_source][:"1password"]
521
+ item = Config.config[:cert_source][:"1password"]
522
+ origfield = file.sub(/^#{http_suffix}\./, '')
523
+ token = ENV['OP_SERVICE_ACCOUNT_TOKEN']
524
+
525
+ field = origfield
526
+ if Config.config[:cert_source]["1password-fields"]
527
+ field = Config.config[:cert_source]["1password-fields"][origfield]
528
+ end
529
+
530
+ raise Heighliner::Error, 'OP_SERVICE_ACCOUNT_TOKEN is not set' unless token
531
+
532
+ Config.info_out.puts " Source: 1Password (item: #{item}, field: #{field} (#{origfield}) )"
533
+
534
+ # take it out
535
+ certstoredir = "#{ENV['CONTEXT_DIR']}/.tmp.certstore"
536
+ tmpfile = "#{certstoredir}/#{file}"
537
+ CommandRunner.run!(Config.out, "mkdir -p #{certstoredir}")
538
+ CommandRunner.run!(Config.out, "op read \"op://#{item}/#{field}\" > #{tmpfile}")
539
+ Config.info_out.puts("wrote into file #{tmpfile}")
540
+ CommandRunner.run!(Config.out, "ls #{tmpfile}")
541
+
542
+ # put it in
543
+ CommandRunner.run! Config.out, "docker run --rm
544
+ -v #{Config.config[:shared_names][:certs]}:/certs
545
+ -v #{certstoredir}:/tmpcert
546
+ alpine
547
+ cp /tmpcert/#{file} /certs/#{file}"
548
+
549
+ unless File.exist?(tmpfile) && File.size(tmpfile).positive?
550
+ raise Heighliner::Error,
551
+ "1Password field '#{field}' not found in item '#{item}'"
552
+ end
553
+
554
+ CommandRunner.run! Config.out, "docker run --rm
555
+ -v #{Config.config[:shared_names][:certs]}:/certs
556
+ -v #{tmpfile}:/cert_source
557
+ alpine cp /cert_source /certs/#{file}"
558
+ CommandRunner.run!(Config.out, "rm #{tmpfile}")
559
+ end
560
+ end
561
+
562
+ def prepare_cert_volume!
563
+ Config.info_out.puts 'Preparing certificate volume'
564
+ create_if_volume_not_exist Config.config[:shared_names][:certs]
565
+ return unless Config.config[:cert_source]
566
+
567
+ %w[
568
+ chain.pem
569
+ crt
570
+ key
571
+ ].each do |file_ext|
572
+ copy_keyfile("#{http_suffix}.#{file_ext}")
573
+ end
574
+ Config.info_out.puts 'Certificate loading complete'
575
+ end
576
+
577
+ def selenium_node_image
578
+ return ENV['OVERRIDE_SELENIUM_NODE_IMAGE'] unless ENV['OVERRIDE_SELENIUM_NODE_IMAGE'].nil?
579
+
580
+ if RUBY_PLATFORM.start_with?('arm64') || RUBY_PLATFORM.start_with?('aarch64')
581
+ # use the seleniarm image because its more stable in arm procs
582
+ # somehow the x64 image does not do well under qemu under arm
583
+ return 'seleniarm/standalone-chromium'
584
+ end
585
+
586
+ # default to x64 image
587
+ 'selenium/standalone-chrome-debug'
588
+ end
589
+
590
+ def home_dir_loc
591
+ return ENV['_HEIGHLINER_USER_HOME'] if ENV['_HEIGHLINER_POS'] == 'docker'
592
+
593
+ ENV['HOME']
594
+ end
595
+
596
+ def ensure_setup
597
+ ensure_env
598
+
599
+ setup if network.nil?
600
+
601
+ create_if_network_not_exist Config.config[:networkname]
602
+ if_container_dead Config.config[:shared_names][:nginx] do
603
+ prepare_cert_volume!
604
+ end
605
+ run_if_dead(
606
+ Config.config[:shared_names][:nginx],
607
+ "docker run -d
608
+ -p 80:80
609
+ -p 443:443
610
+ -v #{Config.config[:shared_names][:certs]}:/etc/nginx/certs
611
+ -v /var/run/docker.sock:/tmp/docker.sock:ro
612
+ --privileged
613
+ --name #{Config.config[:shared_names][:nginx]}
614
+ --network #{Config.config[:networkname]}
615
+ jwilder/nginx-proxy"
616
+ )
617
+
618
+ innerdnsconffile = "#{ENV['HOME']}/.heighliner/dnsconf"
619
+ outerdnsconffile = "#{home_dir_loc}/.heighliner/dnsconf"
620
+ File.write(innerdnsconffile, <<~HOSTS)
621
+ log-queries
622
+ no-resolv
623
+ server=8.8.8.8
624
+ server=1.1.1.1
625
+ address=/.#{http_suffix}/#{ip_of_container(Config.config[:shared_names][:nginx])}
626
+ HOSTS
627
+
628
+ run_if_dead(
629
+ Config.config[:shared_names][:dns],
630
+ "docker run -d
631
+ --name #{Config.config[:shared_names][:dns]}
632
+ --network #{Config.config[:networkname]}
633
+ -v #{outerdnsconffile}:/etc/dnsmasq.conf:ro
634
+ degica/dnsmasq
635
+ "
636
+ )
637
+
638
+ start_chrome_container
639
+ end
640
+
641
+ def start_chrome_container
642
+ run_if_dead(
643
+ Config.config[:shared_names][:chrome],
644
+ "docker run -d
645
+ -p 5900:5900
646
+ --shm-size='2g'
647
+ --name #{Config.config[:shared_names][:chrome]}
648
+ --network #{Config.config[:networkname]}
649
+ --dns #{ip_of_container(Config.config[:shared_names][:dns])}
650
+ #{selenium_node_image}"
651
+ )
652
+ end
653
+
654
+ def ip_of_container(containername)
655
+ networkname = ".NetworkSettings.Networks.#{Config.config[:networkname]}.IPAddress"
656
+ `docker inspect -f '{{#{networkname}}}' #{containername}`.chomp
657
+ end
658
+
659
+ def network
660
+ `docker network inspect #{Config.config[:networkname]} 2>/dev/null`
661
+ end
662
+
663
+ def container_dead?(container)
664
+ x = JSON.parse(`docker inspect #{container} 2>/dev/null`)
665
+ x.empty? || x[0]['State']['Running'] == false
666
+ end
667
+
668
+ def if_container_dead(container)
669
+ return unless container_dead?(container)
670
+
671
+ yield if block_given?
672
+ end
673
+
674
+ def create_if_volume_not_exist(vol)
675
+ x = JSON.parse(`docker volume inspect #{vol} 2>/dev/null`)
676
+ return unless x.empty?
677
+
678
+ CommandRunner.run! Config.out, "docker volume create #{vol}"
679
+ end
680
+
681
+ def create_if_network_not_exist(net)
682
+ out = `docker inspect #{net} 2>/dev/null`
683
+ out = "[]" if out.strip.empty?
684
+ x = JSON.parse(out)
685
+ return unless x.empty?
686
+
687
+ CommandRunner.run! Config.out, "docker network create #{net}"
688
+ end
689
+
690
+ def run_if_dead(container, command)
691
+ if_container_dead container do
692
+ Config.info_out.puts "Starting up #{container}"
693
+ killrm container
694
+ CommandRunner.run Config.out, command
695
+ end
696
+ end
697
+
698
+ def envname
699
+ Config.config[:envnames][Config.work_dir]
700
+ end
701
+
702
+ def save_config
703
+ File.write(Config.config_file, Config.config.to_yaml)
704
+ end
705
+
706
+ def killrm(container)
707
+ x = JSON.parse(`docker inspect #{container} 2>/dev/null`)
708
+ return if x.empty?
709
+
710
+ CommandRunner.run Config.out, "docker kill #{container}" if x[0]['State'] && x[0]['State']['Running'] == true
711
+ CommandRunner.run Config.out, "docker rm #{container}" if x[0]['State']
712
+ end
713
+ end
714
+ end