kaiser 0.0.0 → 0.6.4

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