exe/app-rb in app-rb-0.1.0 vs exe/app-rb in app-rb-0.2.0

- old
+ new

@@ -1,313 +1,5 @@ #!/usr/bin/env ruby -require 'json' -require 'yaml' -require 'pp' -require 'open3' -Thread.abort_on_exception = true +require "bundler/setup" +require "app-rb" -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} <yml> <command>" - 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 <<EOF - set -e - tmpfile=\\$(mktemp /tmp/git-ssh-#{CONFIG["app"]}.XXXXXX) - echo '#!/bin/sh' > \\$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 <<EOF - set -e - cd #{CONFIG["app"]}-cache - #{(CONFIG["image"]["pre_build"] || []).join("\n")} - docker build -t #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} . - docker push #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} - echo Done.\nEOF" - end - vv = Time.now.to_i - new_service = "#{CONFIG["app"]}-#{vv}" - - (CONFIG["deploy"]["pre"] || []).each_with_index do |section, index| - puts blue("+++ PRE: #{section.inspect}") - n = node(section["constraint"] || CONFIG["deploy"]["constraint"]) - puts "node=#{n.inspect}" - do_it "ssh #{CONFIG["user"]}@#{n.ip} docker run " + - "--label app=#{CONFIG["app"]} " + - "--label service=#{new_service} " + - "--name #{CONFIG["app"]}-pre-#{vv}-#{index} " + - "#{(CONFIG["env"] || {}).map { |k, v| "-e #{k}='#{v}'" }.join(" ")} " + - "#{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} #{section["cmd"]}" - do_it "ssh #{CONFIG["user"]}@#{n.ip} docker rm #{CONFIG["app"]}-pre-#{vv}-#{index}" - end - - deploy_nodes = nodes(CONFIG["deploy"]["constraint"]) - - puts blue("+++ PULL") - pull_threads = [] - deploy_nodes.each do |node| - pull_threads << Thread.new do - host = "#{CONFIG["user"]}@#{node.ip}" - Open3.popen2e("ssh #{host} docker pull #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash}") { |i,o,w| - while line = o.gets do - puts "[#{node.name}] " + line - end - raise "FATAL" unless w.value.success? - } - end - end - pull_threads.each(&:join) - - plan = {} - if CONFIG["deploy"]["per"] - amount = CONFIG["deploy"]["per"]*deploy_nodes.count - else - amount = CONFIG["deploy"]["amount"] - end - - amount.times do |index| - ip = deploy_nodes[index % deploy_nodes.length].ip - plan[ip] ||= [] - plan[ip].push(index) - end - - puts blue("+++ DEPLOY") - deploy_threads = [] - deploy_nodes.each do |node| - deploy_threads << Thread.new do - host = "#{CONFIG["user"]}@#{node.ip}" - - puts "[#{node.name}] run" - (plan[node.ip] || []).each do |index| - port = nil - 10.times do - a = MIN_PORT + rand(MAX_PORT - MIN_PORT) - if just_cmd("ssh #{host} ss -ln src :#{a} | fgrep -c ':#{a}'", true) == "0" - port = a - break - end - end - raise "Dont find free port :-((" unless port - puts "[#{node.name}] port=#{port}" - - do_it("ssh #{host} docker run -d " + - "--label app=#{CONFIG["app"]} " + - "--label service=#{new_service} " + - "--name=#{CONFIG["app"]}-#{vv}-#{index} " + - (CONFIG["env"] || {}).map { |k, v| "-e #{k}='#{v}'" }.join(" ") + " " + - "--restart unless-stopped " + - "-p #{node.ip}:#{port}:#{CONFIG["deploy"]["port"]} " + - "#{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} " + - "#{CONFIG["deploy"]["cmd"]}") - - do_it %(curl -s -X PUT #{node.ip}:8500/v1/agent/service/register -d'{ - "Id": "#{CONFIG["app"]}-#{vv}-#{index}", - "Name": "#{new_service}", - "Port": #{port}, - "Check": { - "DeregisterCriticalServiceAfter": "20m", - "Interval": "7s", - "HTTP": "http://#{node.ip}:#{port}#{CONFIG["deploy"]["check_url"] || "/"}" - } - }') - end - - puts blue("+++ CONSUL wait for #{node.name}") - loop do - statuses = JSON.load(just_cmd("curl -s #{node.ip}:8500/v1/health/service/#{CONFIG["app"]}-#{vv}")).select { |s| - s["Node"]["Address"] == node.ip - }.flat_map { |s| s["Checks"] }.map { |c| c["Status"] } - puts "#{node.name} => #{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 - +AppRb::Cli.new(ARGV).run