class AppRb::Command

Public Class Methods

new(config) click to toggle source
# File lib/app-rb/command.rb, line 3
def initialize(config)
  @config = config
end

Public Instance Methods

cd() click to toggle source
# File lib/app-rb/command.rb, line 172
def cd
  hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
  raise "FATAL: app is not started?" if hash == ""
  run_nodes = @config.nodes(@config.run["constraint"])
  AppRb::Util.do_it "ssh -t #{@config.user}@#{run_nodes.sample.ip} bash --login"
end
clean() click to toggle source
# File lib/app-rb/command.rb, line 152
def clean
  base = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "base")
  ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
  stop_services(ips, base)
end
deploy(target) click to toggle source
# File lib/app-rb/command.rb, line 7
def deploy(target)
  @base = "#{@config.app}-#{Time.now.to_i}"
  start_at = Time.now
  user = AppRb::Util.just_cmd("git config user.name")

  # init
  current_hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
  build_nodes = @config.nodes(@config.image["constraint"])
  if build_nodes.empty?
    puts "FATAL ERROR: no build nodes found"
    exit -1
  end
  pre_payloads = @config.pre_deploy.map do |pre|
    payload = {
      "nodes" => @config.nodes(pre["constraint"]),
      "cmd" => pre["cmd"],
      "opts" => pre["opts"] || [],
    }
    if payload["nodes"].empty?
      puts "FATAL ERROR: no pre deploy nodes found"
      exit -1
    end
    payload
  end
  deploy_payloads = @config.deploy.map { |key, section|
    nodes = @config.nodes(section["constraint"])
    if nodes.empty?
      puts "FATAL ERROR: no deploy `#{key}` nodes found"
      exit -1
    end
    {
      "key" => key,
      "nodes" => nodes,
      "amount" => (section["per"] ? section["per"]*nodes.count : section["amount"]),
      "cmd" => section["cmd"],
      "port" => section["port"],
      "check_url" => section["check_url"],
      "opts" => section["opts"] || [],
    }
  }
  if @config.run["constraint"]
    run_nodes = @config.nodes(@config.run["constraint"])
  else
    run_nodes = deploy_payloads.flat_map { |payload| payload["nodes"] }.uniq
  end
  if run_nodes.empty?
    puts "FATAL ERROR: no run nodes found"
    exit -1
  end
  cron_payloads = @config.cron.map { |key, section|
    nodes = @config.nodes(section["constraint"])
    if nodes.empty?
      puts "FATAL ERROR: no cron `#{key}` nodes found"
      exit -1
    end
    {
      "key" => key,
      "nodes" => nodes,
      "cmd" => section["cmd"],
      "at" => "#{section["minute"] || "*"} #{section["hour"] || "*"} #{section["day"] || "*"} #{section["month"] || "*"} #{section["weekday"] || "*"}"
    }
  }
  old_ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
  new_ips = (
    build_nodes.map(&:ip) +
    pre_payloads.flat_map { |p| p["nodes"].map(&:ip) } +
    deploy_payloads.flat_map { |p| p["nodes"].map(&:ip) } +
    run_nodes.map(&:ip) +
    cron_payloads.flat_map { |p| p["nodes"].map(&:ip) }
  ).uniq
  ips = (old_ips + new_ips).uniq
  AppRb::Util::Consul.kv_set(@config.consul, @config.app, "nodes", ips.join(","))

  # pre
  new_hash = prepare_image(build_nodes, target)
  if @config.slack?
    notify_slack(@config.slack_url, @config.slack_channel, "#{user} start deploy *#{@config.app}* - https://github.com/#{@config.image["repo"]}/compare/#{current_hash.to_s[0..6]}...#{new_hash.to_s[0..6]}")
  end
  pre_deploy(pre_payloads, new_hash)
  stop_bg_jobs(ips)

  # deploy
  do_deploy(deploy_payloads, new_hash)

  # switch
  blue_green(deploy_payloads, new_hash)

  # update one time scripts
  one_time_scripts(run_nodes, ips, new_hash)

  # update crons
  set_crons(cron_payloads, ips, new_hash)

  # clean
  stop_services(ips)
  clean_registry(current_hash, [current_hash, new_hash].uniq)
  remove_old_images(ips, [current_hash, new_hash].uniq)

  # finish
  AppRb::Util::Consul.kv_set(@config.consul, @config.app, "nodes", new_ips.join(","))

  if @config.slack?
    notify_slack(@config.slack_url, @config.slack_channel, "#{user} finish deploy *#{@config.app}* :cat: :cat: :cat: - #{((Time.now.to_f - start_at.to_f)/60).round(1)} minutes")
  end

  puts AppRb::Util.green("Done.")
  if current_hash != "" && !target && current_hash != target
    puts "to rollback fire: app-rb #{ARGV[0]} deploy #{current_hash}"
  end
end
redeploy() click to toggle source
# File lib/app-rb/command.rb, line 158
def redeploy
  hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
  raise "FATAL: app is not started?" if hash == ""
  puts "hash=#{hash}"
  deploy(hash)
end
run(cmd) click to toggle source
# File lib/app-rb/command.rb, line 165
def run(cmd)
  hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
  raise "FATAL: app is not started?" if hash == ""
  run_nodes = @config.nodes(@config.run["constraint"])
  AppRb::Util::Docker.run(@config.user, run_nodes.sample.ip, "#{@config.registry}/#{@config.app}:#{hash}", @config.env, cmd)
end
status() click to toggle source
# File lib/app-rb/command.rb, line 118
def status
  ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
  nodes = @config.nodes.select { |n| ips.index(n.ip) }
  max_name_len = nodes.map { |n| n.name.length }.max
  max_ip_len = nodes.map { |n| n.ip.length }.max
  current_base = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "base")
  current_hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
  current_dockers = nodes.map { |n| 
    AppRb::Util::Docker.ids(@config.user, n.ip, {app: @config.app, build: current_base}).count
  }
  dockers = nodes.map { |n| 
    AppRb::Util::Docker.ids(@config.user, n.ip, {app: @config.app}).count
  }
  puts ""
  puts AppRb::Util.green("App:     ") + @config.app
  puts AppRb::Util.green("Base:    ") + current_base
  puts AppRb::Util.green("Hash:    ") + current_hash
  nodes.each_with_index do |n, i|
    puts(
      " "*5 + n.name.rjust(max_name_len) + 
      " "*2 + n.ip.ljust(max_ip_len) + 
      " "*2 + AppRb::Util.green(current_dockers[i]) + " / " + (dockers[i] - current_dockers[i] == 0 ? "0" : AppRb::Util.red(dockers[i] - current_dockers[i]))
    )
    end
end
stop() click to toggle source
# File lib/app-rb/command.rb, line 144
def stop
  ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
  stop_all(ips)
  ips.each do |ip|
    AppRb::Util::Docker.add_cron(@config.user, ip, "#{@config.registry}/#{@config.app}:#{hash}", @config.env, @config.app, [])
  end
end

Private Instance Methods

blue_green(deploy_payloads, new_hash) click to toggle source
# File lib/app-rb/command.rb, line 275
def blue_green(deploy_payloads, new_hash)
  service_payloads = deploy_payloads.select { |p| p["port"] }
  AppRb::Util::Consul.kv_set(@config.consul, @config.app, "hash", new_hash)
  AppRb::Util::Consul.kv_set(@config.consul, @config.app, "base", @base)
  service_payloads.each do |payload|
    AppRb::Util::Consul.kv_set(@config.consul, @config.app, "services/#{payload["key"]}", "#{@base}-#{payload["key"]}")
  end
  puts "\n" + AppRb::Util.green(">>>>>>>>>>>>>>>  BLUE/GREEN switch <<<<<<<<<<<<<<<") + "\n\n"
  sleep 3
  (AppRb::Util::Consul.kv_keys(@config.consul, @config.app + "/services") - service_payloads.map { |p| p["key"] }).each do |remove|
    AppRb::Util::Consul.kv_unset(@config.consul, @config.app + "/services/#{remove}")
  end
end
clean_registry(current_hash, keep_hashes = []) click to toggle source
# File lib/app-rb/command.rb, line 313
def clean_registry(current_hash, keep_hashes = [])
  puts AppRb::Util.blue("+++ CLEAN REGISTRY")
  AppRb::Util::Registry.clean(@config.registry, @config.app, keep_hashes)
end
do_deploy(deploy_payloads, hash) click to toggle source
# File lib/app-rb/command.rb, line 216
def do_deploy(deploy_payloads, hash)
  puts AppRb::Util.blue("+++ PULL")
  deploy_payloads.flat_map { |p| p["nodes"] }.uniq.map { |node|
    Thread.new do
      AppRb::Util::Docker.pull(@config.user, node.ip, full_image_name(hash))
    end
  }.each(&:join)

  deploy_payloads.each do |payload|
    puts AppRb::Util.blue("+++ DEPLOY '#{payload["key"]}'")

    # naive scheduling
    plan = payload["nodes"].map{ |n| [n.ip, []] }.to_h
    payload["amount"].times do |index|
      ip = payload["nodes"][index % payload["nodes"].length].ip
      plan[ip].push({name: "#{payload["key"]}-#{index}", key: payload["key"]})
    end

    payload["nodes"].map { |node|
      Thread.new do
        (plan[node.ip] || []).each do |desc|
          if payload["port"]
            port = AppRb::Util.get_free_port(@config.user, node.ip)
            puts "[#{node.name}] port=#{port}"
          end

          AppRb::Util::Docker.run_daemon(
            @config.user, node.ip,
            "#{@base}-#{desc[:name]}", full_image_name(hash), payload["cmd"],
            {
              app: @config.app,
              build: @base,
              kind: "daemon",
              key: desc[:key],
              has_port: (payload["port"] ? "yes" : "no"),
            },
            @config.env,
            payload["opts"],
            (payload["port"] ? {"#{node.ip}:#{port}" => payload["port"]} : {})
          )

          if payload["port"]
            AppRb::Util::Consul.register_service(
              node.ip,
              "#{@base}-#{desc[:name]}", "#{@base}-#{payload["key"]}", port, payload["check_url"] || "/",
              [@config.app, @base]
            )
          end
        end
      end
    }.each(&:join)

    if payload["port"]
      puts AppRb::Util.blue("+++ CONSUL wait '#{payload["key"]}'")
      AppRb::Util::Consul.consul_wait(@config.consul, "#{@base}-#{payload["key"]}")
    end
  end
end
full_image_name(hash) click to toggle source
# File lib/app-rb/command.rb, line 185
def full_image_name(hash)
  "#{@config.registry}/#{@config.app}:#{hash}"
end
notify_slack(url, channel, msg) click to toggle source
# File lib/app-rb/command.rb, line 181
def notify_slack(url, channel, msg)
  AppRb::Util.do_it(%(curl -s -X POST --data-urlencode 'payload={"channel": "#{channel}", "username": "deplobot", "parse": "full", "text": "#{msg}"}' #{url}))
end
one_time_scripts(run_nodes, ips, hash) click to toggle source
# File lib/app-rb/command.rb, line 325
def one_time_scripts(run_nodes, ips, hash)
  puts AppRb::Util.blue("+++ ONE TIME SCRIPTS")
  run_nodes.each do |n|
    AppRb::Util::Docker.create_one_time_script(@config.user, n.ip, "#{@config.registry}/#{@config.app}:#{hash}", @config.env, @config.app)
  end
  ips.each do |ip|
    next if run_nodes.map(&:ip).index(ip)
    AppRb::Util::Docker.remove_one_time_script(@config.user, ip, @config.app)
  end
end
pre_deploy(pre_payloads, hash) click to toggle source
# File lib/app-rb/command.rb, line 198
def pre_deploy(pre_payloads, hash)
  pre_payloads.each_with_index do |payload, index|
    puts AppRb::Util.blue("+++ PRE: #{payload["cmd"].inspect}")
    AppRb::Util::Docker.run_batch(
      @config.user, payload["nodes"].sample.ip,
      "#{@base}-pre-#{index}", full_image_name(hash), payload["cmd"],
      {
        app: @config.app,
        build: @base,
        kind: "batch",
        key: "pre-#{index}",
      },
      @config.env,
      payload["opts"]
    )
  end
end
prepare_image(build_nodes, target) click to toggle source
# File lib/app-rb/command.rb, line 189
def prepare_image(build_nodes, target)
  puts AppRb::Util.blue("+++ CLONE or UPDATE repository")
  AppRb::Util::Build.build(
    @config.user, build_nodes.sample.ip,
    @config.image["repo"], @config.image["key"], target || @config.image["target"],
    @config.registry, @config.app, @config.image["pre_build"] || []
  )
end
remove_old_images(ips, keep_hashes = []) click to toggle source
# File lib/app-rb/command.rb, line 318
def remove_old_images(ips, keep_hashes = [])
  puts AppRb::Util.blue("+++ REMOVE OLD IMAGES")
  ips.each do |ip|
    AppRb::Util::Docker.remove_images(@config.user, ip, "#{@config.registry}/#{@config.app}", keep_hashes)
  end
end
set_crons(cron_payloads, ips, hash) click to toggle source
# File lib/app-rb/command.rb, line 336
def set_crons(cron_payloads, ips, hash)
  puts AppRb::Util.blue("+++ CRONS")
  ips.each do |ip|
    crons = cron_payloads.select { |section|
      section["nodes"].map(&:ip).index(ip)
    }.map { |section|
      {"cmd" => section["cmd"], "at" => section["at"], "key" => section["key"]}
    }
    AppRb::Util::Docker.add_cron(@config.user, ip, "#{@config.registry}/#{@config.app}:#{hash}", @config.env, @config.app, crons)
  end
end
stop_all(ips) click to toggle source
# File lib/app-rb/command.rb, line 304
def stop_all(ips)
  puts AppRb::Util.blue("+++ STOP")
  AppRb::Util::Consul.remove_services(@config.consul, [@config.app])
  ips.each do |ip|
    AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app})
  end
  AppRb::Util::Consul.kv_unset(@config.consul, @config.app)
end
stop_bg_jobs(ips) click to toggle source
# File lib/app-rb/command.rb, line 289
def stop_bg_jobs(ips)
  puts AppRb::Util.blue("+++ STOP OLD backgroud jobs")
  ips.each do |ip|
    AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app, has_port: "no"})
  end
end
stop_services(ips, base = @base) click to toggle source
# File lib/app-rb/command.rb, line 296
def stop_services(ips, base = @base)
  puts AppRb::Util.blue("+++ STOP services")
  AppRb::Util::Consul.remove_services(@config.consul, [@config.app], base)
  ips.each do |ip|
    AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app, has_port: "yes"}, {build: base})
  end
end