#!/usr/bin/env ruby require 'json' require 'yaml' require 'pp' require 'open3' Thread.abort_on_exception = true def yellow(txt); "\e[0;33m#{txt}\e[0m"; end def red(txt); "\e[0;31m#{txt}\e[0m"; end def green(txt); "\e[0;32m#{txt}\e[0m"; end def blue(txt); "\e[0;34m#{txt}\e[0m"; end def do_it(cmd) puts "[exec] #{cmd}" system(cmd) unless $?.success? puts red("FATAL :(") exit end end def just_cmd(cmd, skip_exit_status = false) puts "[exec] #{cmd}" output = `#{cmd}` if $?.success? || skip_exit_status output.strip else puts red(output) puts red("FATAL :(") exit end end if ARGV.count < 2 puts "Just deploy your apps with docker and consul. Nothing else." puts "" puts " #{$0} " puts "" puts " deploy [hash] - deploy new version of app" puts " status - status of app" puts " stop - stop app" exit end CONFIG = YAML.load(File.read(ARGV[0])) COMMAND = ARGV[1] MIN_PORT = 10_000 MAX_PORT = 50_000 Node = Struct.new(:name, :ip, :roles) NODES = JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/catalog/nodes")).sort_by { |n| n["Node"] }.map { |n| Node.new(n["Node"], n["Address"], (n["Meta"] || {})["roles"].to_s.split(",").reject{ |s| s.to_s.empty? }) } def nodes(constraint) constraint ||= {} out = NODES if constraint["role"] out = out.select { |n| n.roles.index(constraint["role"]) } end if constraint["name"] out = out.select { |n| n.name == constraint["name"] } end out end def node(constraint) nodes(constraint).sample end def deploy(target = nil) build_node = node(CONFIG["image"]["constraint"]) puts "build_node=#{build_node.to_h.inspect}" puts blue("+++ CLONE or UPDATE repository") do_it "ssh #{CONFIG["user"]}@#{build_node.ip} bash < \\$tmpfile echo 'exec /usr/bin/ssh -o StrictHostKeyChecking=no -i #{CONFIG["image"]["key"]} \"\\$@\"' >> \\$tmpfile chmod +x \\$tmpfile export GIT_SSH=\\$tmpfile if [ -d #{CONFIG["app"]}-cache ]; then echo update cache... cd #{CONFIG["app"]}-cache git checkout . && git clean -dfx && git checkout master && git pull git branch | grep -v master | xargs -r git branch -D else echo clone... git clone git@github.com:#{CONFIG["image"]["repo"]} #{CONFIG["app"]}-cache && cd #{CONFIG["app"]}-cache fi git checkout #{target || CONFIG["image"]["target"]} rm \\$tmpfile\nEOF" puts blue("+++ calculate HASH and VERSION") hash = ARGV[2] || `ssh #{CONFIG["user"]}@#{build_node.ip} 'cd #{CONFIG["app"]}-cache && git rev-parse HEAD'`.strip puts "hash: #{hash}" o = JSON.load(`curl -s https://#{CONFIG["registry"]}/v2/#{CONFIG["app"]}/tags/list`) tags = o.is_a?(Hash) && o["errors"] ? [] : o["tags"] puts "tags: #{JSON.dump(tags)}" unless tags.index(hash) puts blue("+++ BUILD image") do_it "ssh #{CONFIG["user"]}@#{build_node.ip} bash < #{statuses.inspect}" break if statuses.uniq == ["passing"] or (plan[node.ip] || []).empty? sleep 3 end end end deploy_threads.each(&:join) current_service = just_cmd("curl #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw") current_hash = just_cmd("curl #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash?raw") puts "CURRENT_SERVICE=#{current_service}" puts "CURRENT_HASH=#{current_hash}" puts "NEW_SERVICE=#{new_service}" do_it "curl -X PUT #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service -d#{new_service}" do_it "curl -X PUT #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash -d#{hash}" puts "\n\n" + green(">>>>>>>>>>>>>>> BLUE/GREEN switch <<<<<<<<<<<<<<<") sleep 3 puts blue("+++ STOP OLD containers") JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/catalog/services")).keys.select { |service| service != new_service && service =~ /^#{CONFIG["app"]}-\d+$/ }.each do |service| JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/health/service/#{service}")).each do |s| do_it %(curl -s -X DELETE #{s["Node"]["Address"]}:8500/v1/agent/service/deregister/#{s["Service"]["ID"]}) end end NODES.each do |n| keep_ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]} -f label=service=#{new_service}").split("\n") all_ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]}").split("\n") if (all_ids - keep_ids).length > 0 do_it("ssh #{CONFIG["user"]}@#{n.ip} docker stop #{(all_ids - keep_ids).join(" ")}") do_it("ssh #{CONFIG["user"]}@#{n.ip} docker rm #{(all_ids - keep_ids).join(" ")}") end end puts blue("+++ CLEAN REGISTRY") (tags - [hash, current_hash]).each do |hash| digest = just_cmd("curl -s --head -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' https://#{CONFIG["registry"]}/v2/#{CONFIG["app"]}/manifests/#{hash} | grep Docker-Content-Digest | cut -d' ' -f2") puts "digest = #{digest}" system "curl -X DELETE https://#{CONFIG["registry"]}/v2/#{CONFIG["app"]}/manifests/#{digest}" end puts green("Done.") if current_hash != "" && !target puts "to rollback execute: #{$0} #{ARGV[0]} deploy #{current_hash}" end end def print_status max_name_len = NODES.map { |n| n.name.length }.max max_ip_len = NODES.map { |n| n.ip.length }.max max_roles_len = NODES.map { |n| n.roles.inspect.length }.max current_service = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw") current_hash = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash?raw") current_dockers = NODES.map { |n| just_cmd("ssh #{CONFIG["user"]}@#{n.ip} 'docker ps -q -f label=app=#{CONFIG["app"]} -f label=service=#{current_service} | wc -l'").to_i } dockers = NODES.map { |n| just_cmd("ssh #{CONFIG["user"]}@#{n.ip} 'docker ps -q -f label=app=#{CONFIG["app"]} | wc -l'").to_i } puts "" puts green("App: ") + CONFIG["app"] puts green("Service: ") + current_service puts 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 + n.roles.inspect.ljust(max_roles_len) + " "*2 + green(current_dockers[i]) + " / " + (dockers[i] - current_dockers[i] == 0 ? "0" : red(dockers[i] - current_dockers[i])) ) end end def do_stop current_service = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw") if current_service != "" JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/health/service/#{current_service}")).each do |s| do_it %(curl -s -X DELETE #{s["Node"]["Address"]}:8500/v1/agent/service/deregister/#{s["Service"]["ID"]}) end end NODES.map { |n| ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]}").gsub("\n", " ") if ids != "" do_it "ssh #{CONFIG["user"]}@#{n.ip} docker stop #{ids}" do_it "ssh #{CONFIG["user"]}@#{n.ip} docker rm #{ids}" end } do_it "curl -X DELETE #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}?recurse" puts "" end if COMMAND == "deploy" || COMMAND == "d" deploy(ARGV[2]) elsif COMMAND == "status" || COMMAND == "s" print_status elsif COMMAND == "stop" do_stop else puts "FATAL: unknown command '#{COMMAND}'" end