class Kaiser::Cli

The commandline

Public Class Methods

all_subcommands_usage() click to toggle source
# File lib/kaiser/cli.rb, line 80
def self.all_subcommands_usage
  output = ''

  @subcommands.each do |name, klass|
    name_s = name.to_s

    output += name_s + "\n"
    output += name_s.gsub(/./, '-')
    output += "\n"
    output += klass.usage
    output += "\n\n"
  end

  output
end
register(name, klass) click to toggle source
# File lib/kaiser/cli.rb, line 43
def self.register(name, klass)
  @subcommands ||= {}
  @subcommands[name] = klass.new
end
run_command(name, global_opts) click to toggle source
# File lib/kaiser/cli.rb, line 48
def self.run_command(name, global_opts)
  cmd = @subcommands[name]
  opts = cmd.define_options(global_opts + cmd.class.options)

  # The define_options method has stripped all arguments from the cli so now
  # all that we're left with in ARGV are the subcommand to be run and possibly
  # its own subcommands. We remove the subcommand here so each subcommand can
  # easily use ARGV.shift to access its own subcommands.
  ARGV.shift

  Kaiser::Config.load(Dir.pwd)

  # We do all this work in here instead of the exe/kaiser file because we
  # want -h options to output before we check if a Kaiserfile exists.
  # If we do it in exe/kaiser, people won't be able to check help messages
  # unless they create a Kaiserfile firest.
  if opts[:quiet]
    Config.out = File.open(File::NULL, 'w')
    Config.info_out = File.open(File::NULL, 'w')
  elsif opts[:verbose] || Config.always_verbose?
    Config.out = $stderr
    Config.info_out = Kaiser::AfterDotter.new(dotter: Kaiser::Dotter.new)
  else
    Config.out = Kaiser::Dotter.new
    Config.info_out = Kaiser::AfterDotter.new(dotter: Config.out)
  end

  cmd.set_config

  cmd.execute(opts)
end

Public Instance Methods

define_options(global_opts = []) click to toggle source

At first I did this in the constructor but the problem with that is Optimist will parse the entire commandline for the first Cli command registered. That means no matter what you call -h or –help on, it will always return the help for the first subcommand. Fixed this by only running define_options when a command is run. We can't just run the constructor at that point because we need each Cli class to be constructed in the beginning so we can add their usage text to the output of `kaiser -h`.

# File lib/kaiser/cli.rb, line 31
def define_options(global_opts = [])
  # We can't just call usage within the options block because that actually shifts
  # the scope to Optimist::Parser. We can still reference variables but we can't
  # call instance methods of a Kaiser::Cli class.
  u = usage
  Optimist.options do
    banner u

    global_opts.each { |o| opt *o }
  end
end
set_config() click to toggle source
# File lib/kaiser/cli.rb, line 10
def set_config
  # This is here for backwards compatibility since it can be used in Kaiserfiles.
  # It would be a good idea to deprecate this and make it more abstract.
  @work_dir = Config.work_dir
  @config_dir = Config.work_dir
  @config_file = Config.config_file
  @kaiserfile = Config.kaiserfile
  @config = Config.config
  @out = Config.out
  @info_out = Config.info_out

  @kaiserfile.validate!
end
start_services() click to toggle source
# File lib/kaiser/cli.rb, line 102
def start_services
  services.each do |service|
    Config.info_out.puts "Starting service: #{service.name}"
    run_if_dead(
      service.shared_name,
      "docker run -d
        --name #{service.shared_name}
        --network #{Config.config[:networkname]}
        #{service.image}"
    )
  end
end
stop_app() click to toggle source
# File lib/kaiser/cli.rb, line 96
def stop_app
  Config.info_out.puts 'Stopping application'
  killrm app_container_name
  stop_services
end
stop_services() click to toggle source
# File lib/kaiser/cli.rb, line 115
def stop_services
  services.each do |service|
    Config.info_out.puts "Stopping service: #{service.name}"
    killrm service.shared_name
  end
end

Private Instance Methods

app_container_name() click to toggle source
# File lib/kaiser/cli.rb, line 445
def app_container_name
  "#{envname}-app"
end
app_expose() click to toggle source
# File lib/kaiser/cli.rb, line 437
def app_expose
  Config.kaiserfile.port
end
app_params() click to toggle source
# File lib/kaiser/cli.rb, line 421
def app_params
  eval_template Config.kaiserfile.params
end
app_port() click to toggle source
# File lib/kaiser/cli.rb, line 433
def app_port
  Config.config[:envs][envname][:app_port]
end
attach_app() click to toggle source
# File lib/kaiser/cli.rb, line 232
def attach_app
  cmd = (ARGV || []).join(' ')
  killrm app_container_name

  attach_mounts = Config.kaiserfile.attach_mounts
  volumes = attach_mounts.map { |from, to| "-v #{`pwd`.chomp}/#{from}:#{to}" }.join(' ')

  system "docker run -ti
    --name #{app_container_name}
    --network #{network_name}
    --dns #{ip_of_container(Config.config[:shared_names][:dns])}
    --dns-search #{http_suffix}
    -p #{app_port}:#{app_expose}
    -e DEV_APPLICATION_HOST=#{envname}.#{http_suffix}
    -e VIRTUAL_HOST=#{envname}.#{http_suffix}
    -e VIRTUAL_PORT=#{app_expose}
    #{volumes}
    #{app_params}
    kaiser:#{envname}-#{current_branch} #{cmd}".tr("\n", ' ')

  Config.out.puts 'Cleaning up...'
end
check_db_image_exists(name) click to toggle source
# File lib/kaiser/cli.rb, line 166
def check_db_image_exists(name)
  return if File.exist?(db_image_path(name))

  Optimist.die 'No saved state exists with that name'
end
container_dead?(container) click to toggle source
# File lib/kaiser/cli.rb, line 553
def container_dead?(container)
  x = JSON.parse(`docker inspect #{container} 2>/dev/null`)
  return true if x.length.zero? || x[0]['State']['Running'] == false
end
copy_keyfile(file) click to toggle source
# File lib/kaiser/cli.rb, line 467
def copy_keyfile(file)
  if Config.config[:cert_source][:folder]
    CommandRunner.run! Config.out, "docker run --rm
      -v #{Config.config[:shared_names][:certs]}:/certs
      -v #{Config.config[:cert_source][:folder]}:/cert_source
      alpine cp /cert_source/#{file} /certs/#{file}"

  elsif Config.config[:cert_source][:url]
    CommandRunner.run! Config.out, "docker run --rm
      -v #{Config.config[:shared_names][:certs]}:/certs
      alpine wget #{Config.config[:cert_source][:url]}/#{file}
        -O /certs/#{file}"
  end
end
create_if_network_not_exist(net) click to toggle source
# File lib/kaiser/cli.rb, line 571
def create_if_network_not_exist(net)
  x = JSON.parse(`docker inspect #{net} 2>/dev/null`)
  return unless x.length.zero?

  CommandRunner.run! Config.out, "docker network create #{net}"
end
create_if_volume_not_exist(vol) click to toggle source
# File lib/kaiser/cli.rb, line 564
def create_if_volume_not_exist(vol)
  x = JSON.parse(`docker volume inspect #{vol} 2>/dev/null`)
  return unless x.length.zero?

  CommandRunner.run! Config.out, "docker volume create #{vol}"
end
current_branch() click to toggle source
# File lib/kaiser/cli.rb, line 453
def current_branch
  `git branch | grep \\* | cut -d ' ' -f2`.chomp.gsub(/[^\-_0-9a-z]+/, '-')
end
current_branch_db_image_dir() click to toggle source
# File lib/kaiser/cli.rb, line 214
def current_branch_db_image_dir
  "#{Config.config_dir}/databases/#{envname}/#{current_branch}"
end
db_commands() click to toggle source
# File lib/kaiser/cli.rb, line 393
def db_commands
  eval_template Config.kaiserfile.database[:commands]
end
db_container_name() click to toggle source
# File lib/kaiser/cli.rb, line 449
def db_container_name
  "#{envname}-db"
end
db_data_directory() click to toggle source
# File lib/kaiser/cli.rb, line 397
def db_data_directory
  Config.kaiserfile.database[:data_dir]
end
db_expose() click to toggle source
# File lib/kaiser/cli.rb, line 381
def db_expose
  Config.kaiserfile.database[:port]
end
db_image() click to toggle source
# File lib/kaiser/cli.rb, line 389
def db_image
  Config.kaiserfile.database[:image]
end
db_image_path(name) click to toggle source
# File lib/kaiser/cli.rb, line 218
def db_image_path(name)
  if name.start_with?('./')
    path = "#{`pwd`.chomp}/#{name.sub('./', '')}"
    Config.info_out.puts "Database image path is: #{path}"
    return path
  end
  FileUtils.mkdir_p current_branch_db_image_dir
  "#{current_branch_db_image_dir}/#{name}.tar.bz"
end
db_params() click to toggle source
# File lib/kaiser/cli.rb, line 385
def db_params
  eval_template Config.kaiserfile.database[:params]
end
db_port() click to toggle source
# File lib/kaiser/cli.rb, line 377
def db_port
  Config.config[:envs][envname][:db_port]
end
db_reset_command() click to toggle source
# File lib/kaiser/cli.rb, line 425
def db_reset_command
  eval_template Config.kaiserfile.database_reset_command
end
db_volume_name() click to toggle source
# File lib/kaiser/cli.rb, line 441
def db_volume_name
  "#{envname}-database"
end
db_waitscript() click to toggle source
# File lib/kaiser/cli.rb, line 405
def db_waitscript
  eval_template Config.kaiserfile.database[:waitscript]
end
db_waitscript_params() click to toggle source
# File lib/kaiser/cli.rb, line 409
def db_waitscript_params
  eval_template Config.kaiserfile.database[:waitscript_params]
end
default_db_image() click to toggle source
# File lib/kaiser/cli.rb, line 228
def default_db_image
  db_image_path('.default')
end
delete_db_volume() click to toggle source
# File lib/kaiser/cli.rb, line 210
def delete_db_volume
  CommandRunner.run Config.out, "docker volume rm #{db_volume_name}"
end
docker_build_args() click to toggle source
# File lib/kaiser/cli.rb, line 417
def docker_build_args
  Config.kaiserfile.docker_build_args
end
docker_file_contents() click to toggle source
# File lib/kaiser/cli.rb, line 413
def docker_file_contents
  eval_template Config.kaiserfile.docker_file_contents
end
ensure_db_volume() click to toggle source
# File lib/kaiser/cli.rb, line 124
def ensure_db_volume
  create_if_volume_not_exist db_volume_name
end
ensure_env() click to toggle source
# File lib/kaiser/cli.rb, line 457
def ensure_env
  return unless envname.nil?

  Optimist.die('No environment? Please use kaiser init <name>')
end
ensure_setup() click to toggle source
# File lib/kaiser/cli.rb, line 495
def ensure_setup
  ensure_env

  setup if network.nil?

  create_if_network_not_exist Config.config[:networkname]
  if_container_dead Config.config[:shared_names][:nginx] do
    prepare_cert_volume!
  end
  run_if_dead(
    Config.config[:shared_names][:redis],
    "docker run -d
      --name #{Config.config[:shared_names][:redis]}
      --network #{Config.config[:networkname]}
      redis:alpine"
  )
  run_if_dead(
    Config.config[:shared_names][:chrome],
    "docker run -d
      -p 5900:5900
      --name #{Config.config[:shared_names][:chrome]}
      --network #{Config.config[:networkname]}
      selenium/standalone-chrome-debug"
  )
  run_if_dead(
    Config.config[:shared_names][:nginx],
    "docker run -d
      -p 80:80
      -p 443:443
      -v #{Config.config[:shared_names][:certs]}:/etc/nginx/certs
      -v /var/run/docker.sock:/tmp/docker.sock:ro
      --privileged
      --name #{Config.config[:shared_names][:nginx]}
      --network #{Config.config[:networkname]}
      jwilder/nginx-proxy"
  )
  run_if_dead(
    Config.config[:shared_names][:dns],
    "docker run -d
      --name #{Config.config[:shared_names][:dns]}
      --network #{Config.config[:networkname]}
      --privileged
      -v /var/run/docker.sock:/docker.sock:ro
      davidsiaw/docker-dns
      --domain #{http_suffix}
      --record :#{ip_of_container(Config.config[:shared_names][:nginx])}"
  )
end
envname() click to toggle source
# File lib/kaiser/cli.rb, line 586
def envname
  Config.config[:envnames][Config.work_dir]
end
eval_template(value) click to toggle source
# File lib/kaiser/cli.rb, line 429
def eval_template(value)
  ERB.new(value).result(binding)
end
http_suffix() click to toggle source
# File lib/kaiser/cli.rb, line 463
def http_suffix
  Config.config[:http_suffix] || 'lvh.me'
end
if_container_dead(container) { || ... } click to toggle source
# File lib/kaiser/cli.rb, line 558
def if_container_dead(container)
  return unless container_dead?(container)

  yield if block_given?
end
ip_of_container(containername) click to toggle source
# File lib/kaiser/cli.rb, line 544
def ip_of_container(containername)
  networkname = ".NetworkSettings.Networks.#{Config.config[:networkname]}.IPAddress"
  `docker inspect -f '{{#{networkname}}}' #{containername}`.chomp
end
killrm(container) click to toggle source
# File lib/kaiser/cli.rb, line 594
def killrm(container)
  x = JSON.parse(`docker inspect #{container} 2>/dev/null`)
  return if x.length.zero?

  CommandRunner.run Config.out, "docker kill #{container}" if x[0]['State'] && x[0]['State']['Running'] == true
  CommandRunner.run Config.out, "docker rm #{container}" if x[0]['State']
end
load_db(name) click to toggle source
# File lib/kaiser/cli.rb, line 156
def load_db(name)
  check_db_image_exists(name)
  killrm db_container_name
  CommandRunner.run Config.out, "docker volume rm #{db_volume_name}"
  delete_db_volume
  create_if_volume_not_exist db_volume_name
  load_db_state_from file: db_image_path(name), to_container: db_volume_name
  start_db
end
load_db_state_from(file:, to_container:) click to toggle source
# File lib/kaiser/cli.rb, line 182
def load_db_state_from(file:, to_container:)
  Config.info_out.puts 'Loading database state'
  CommandRunner.run Config.out, "docker run --rm
    -v #{to_container}:#{db_data_directory}
    -v #{file}:#{file}
    ruby:alpine
    tar xvjf #{file} -C #{db_data_directory}
      --strip #{db_data_directory.scan(%r{\/}).count}"
end
network() click to toggle source
# File lib/kaiser/cli.rb, line 549
def network
  `docker network inspect #{Config.config[:networkname]} 2>/dev/null`
end
network_name() click to toggle source
# File lib/kaiser/cli.rb, line 369
def network_name
  Config.config[:networkname]
end
prepare_cert_volume!() click to toggle source
# File lib/kaiser/cli.rb, line 482
def prepare_cert_volume!
  create_if_volume_not_exist Config.config[:shared_names][:certs]
  return unless Config.config[:cert_source]

  %w[
    chain.pem
    crt
    key
  ].each do |file_ext|
    copy_keyfile("#{http_suffix}.#{file_ext}")
  end
end
run_blocking_script(image, params, script, &block) click to toggle source
# File lib/kaiser/cli.rb, line 294
def run_blocking_script(image, params, script, &block)
  killrm tmp_db_waiter
  killrm tmp_file_container

  create_if_volume_not_exist tmp_file_volume

  CommandRunner.run! Config.out, "docker create
    -v #{tmp_file_volume}:/tmpvol
    --name #{tmp_file_container} alpine"

  File.write(tmp_waitscript_name, script)

  CommandRunner.run! Config.out, "docker cp
    #{tmp_waitscript_name}
    #{tmp_file_container}:/tmpvol/wait.sh"

  CommandRunner.run!(
    Config.out,
    "docker run --rm -ti
      --name #{tmp_db_waiter}
      --network #{network_name}
      -v #{tmp_file_volume}:/tmpvol
      #{params}
      #{image} sh /tmpvol/wait.sh",
    &block
  )
ensure
  killrm tmp_file_container
  FileUtils.rm(tmp_waitscript_name)
end
run_if_dead(container, command) click to toggle source
# File lib/kaiser/cli.rb, line 578
def run_if_dead(container, command)
  if_container_dead container do
    Config.info_out.puts "Starting up #{container}"
    killrm container
    CommandRunner.run Config.out, command
  end
end
save_config() click to toggle source
# File lib/kaiser/cli.rb, line 590
def save_config
  File.write(Config.config_file, Config.config.to_yaml)
end
save_db(name) click to toggle source
# File lib/kaiser/cli.rb, line 150
def save_db(name)
  killrm db_container_name
  save_db_state_from container: db_volume_name, to_file: db_image_path(name)
  start_db
end
save_db_state_from(container:, to_file:) click to toggle source
# File lib/kaiser/cli.rb, line 172
def save_db_state_from(container:, to_file:)
  Config.info_out.puts 'Saving database state'
  File.write(to_file, '')
  CommandRunner.run Config.out, "docker run --rm
    -v #{container}:#{db_data_directory}
    -v #{to_file}:#{to_file}
    ruby:alpine
    tar cvjf #{to_file} #{db_data_directory}"
end
server_type() click to toggle source
# File lib/kaiser/cli.rb, line 401
def server_type
  Config.kaiserfile.server_type
end
services() click to toggle source
# File lib/kaiser/cli.rb, line 373
def services
  @services ||= Config.kaiserfile.services.map { |name, info| Service.new(envname, name, info) }
end
setup_db() click to toggle source
# File lib/kaiser/cli.rb, line 128
def setup_db
  ensure_db_volume
  start_db
  return if File.exist?(default_db_image)

  # Some databases keep state around, best to clean it.
  stop_db
  delete_db_volume
  start_db

  Config.info_out.puts 'Provisioning database'
  killrm "#{envname}-apptemp"
  CommandRunner.run! Config.out, "docker run -ti
    --rm
    --name #{envname}-apptemp
    --network #{Config.config[:networkname]}
    #{app_params}
    kaiser:#{envname}-#{current_branch} #{db_reset_command}"

  save_db('.default')
end
start_app() click to toggle source
# File lib/kaiser/cli.rb, line 255
def start_app
  start_services

  Config.info_out.puts 'Starting up application'
  killrm app_container_name
  CommandRunner.run! Config.out, "docker run -d
    --name #{app_container_name}
    --network #{network_name}
    --dns #{ip_of_container(Config.config[:shared_names][:dns])}
    --dns-search #{http_suffix}
    -p #{app_port}:#{app_expose}
    -e DEV_APPLICATION_HOST=#{envname}.#{http_suffix}
    -e VIRTUAL_HOST=#{envname}.#{http_suffix}
    -e VIRTUAL_PORT=#{app_expose}
    #{app_params}
    kaiser:#{envname}-#{current_branch}"
  wait_for_app
end
start_db() click to toggle source
# File lib/kaiser/cli.rb, line 197
def start_db
  Config.info_out.puts 'Starting up database'
  run_if_dead db_container_name, "docker run -d
    -p #{db_port}:#{db_expose}
    -v #{db_volume_name}:#{db_data_directory}
    --name #{db_container_name}
    --network #{network_name}
    #{db_params}
    #{db_image}
    #{db_commands}"
  wait_for_db unless db_waitscript.nil?
end
stop_db() click to toggle source
# File lib/kaiser/cli.rb, line 192
def stop_db
  Config.info_out.puts 'Stopping database'
  killrm db_container_name
end
tmp_db_waiter() click to toggle source
# File lib/kaiser/cli.rb, line 282
def tmp_db_waiter
  "#{envname}-dbwait"
end
tmp_dockerfile_name() click to toggle source
# File lib/kaiser/cli.rb, line 278
def tmp_dockerfile_name
  "#{Config.config_dir}/.#{envname}-dockerfile"
end
tmp_file_container() click to toggle source
# File lib/kaiser/cli.rb, line 286
def tmp_file_container
  "#{envname}-tmpfiles"
end
tmp_file_volume() click to toggle source
# File lib/kaiser/cli.rb, line 290
def tmp_file_volume
  "#{envname}-tmpfiles-vol"
end
tmp_waitscript_name() click to toggle source
# File lib/kaiser/cli.rb, line 274
def tmp_waitscript_name
  "#{Config.config_dir}/.#{envname}-dbwaitscript"
end
wait_for_app() click to toggle source
# File lib/kaiser/cli.rb, line 325
    def wait_for_app
      return unless server_type == :http

      Config.info_out.puts 'Waiting for server to start...'

      http_code_extractor = "curl -s -o /dev/null -I -w \"\%{http_code}\" http://#{app_container_name}:#{app_expose}"
      unreachable_test = "#{http_code_extractor} | grep -q 000"

      # This waitscript runs until curl returns a non-unreachable status code
      # and then checks to see if its 200. If its not, it will raise an error.
      wait_script = <<-SCRIPT
        apk update
        apk add curl
        while #{unreachable_test}; do
            echo 'o'
            sleep 1
        done
        echo '#{http_code_extractor}'
        echo $(#{http_code_extractor})
        if [ "$(#{http_code_extractor})" != "200" ]; then
          echo $(#{http_code_extractor})
        else
          echo '!'
        fi
      SCRIPT
      run_blocking_script('alpine', '', wait_script) do |line|
        # This script gets run every line that gets output.
        # The '!' exclamation mark means success
        # Three numbers means a status code has been returned
        # If curl returns an error status the script will cut out and
        # the app container died error will be displayed.
        raise Kaiser::Error, "Failed with HTTP status: #{line}" if line =~ /^[0-9]{3}$/ && line != '200'
        raise Kaiser::Error, 'App container died. Run `kaiser logs` to see why.' if line != '!' && container_dead?(app_container_name)
      end

      Config.info_out.puts 'Started successfully!'
    end
wait_for_db() click to toggle source
# File lib/kaiser/cli.rb, line 363
def wait_for_db
  Config.info_out.puts 'Waiting for database to start...'
  run_blocking_script(db_image, db_waitscript_params, db_waitscript)
  Config.info_out.puts 'Started.'
end