module AppRb class Command def initialize(config) @config = config end 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 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 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 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 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 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 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 private 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 def full_image_name(hash) "#{@config.registry}/#{@config.app}:#{hash}" end 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 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" }, @config.env, payload["opts"] ) end end 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("#{payload["key"]}-#{index}") end payload["nodes"].map { |node| Thread.new do (plan[node.ip] || []).each do |name| 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}-#{name}", full_image_name(hash), payload["cmd"], { app: @config.app, build: @base, kind: "daemon", name: name, 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}-#{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 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 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 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 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 def clean_registry(current_hash, keep_hashes = []) puts AppRb::Util.blue("+++ CLEAN REGISTRY") AppRb::Util::Registry.clean(@config.registry, @config.app, keep_hashes) end 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 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 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 end end