kaiser 0.0.0 → 0.6.0

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