app-rb 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23df7f2417cf94b6957cde15260ae950348d8861
4
- data.tar.gz: 10198299029a90bada242c624e4bfc59bf3c3e1b
3
+ metadata.gz: b45f2f46e1dcd69be448e51337faaded1bd09579
4
+ data.tar.gz: 29460c4f73febf5601a536e76ffe0af0b9ed1c4b
5
5
  SHA512:
6
- metadata.gz: 88b81b190edd57ee17b485e967303421e777f13fa39b22d4d27eed241e86d344ad122abe6db9d1371c8b0f5647ffa87f03550a21837b6ffb3c964bd77f1ba96e
7
- data.tar.gz: 780f0b901f9ed60f2194855617ec896cd986d50a9b7eb2577b93e598b538f99846365c5bb51be38f7267d01b6a6a1475a9b3425344a250ede0acb39f49837106
6
+ metadata.gz: 28186b82f29304966a4faf69fbd533fe5272142b2ca14f45d188b730cab30b340a2fa3debd4a77f46b6b2c4807a52a0315d5975f5c9fba434a7e066850c5bc0c
7
+ data.tar.gz: 7c56418bf676f3f7dd4ebe60c3c919e515d3b6cf7bfad7d610527e67afeed04909937d52bde1eee24acebda48adc75545e9231da583e7be0fd839f9d595e422c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## App-rb 0.2.0 (March 25, 2017) ##
2
+
3
+ * Add `opts` stanza for `pre_deploy` and `deploy` sections.
4
+ * Remember work nodes.
5
+ * Add `restart` and `clean` cli commands.
6
+ * Add backgound jobs.
7
+ * Add `version` config param.
8
+ * Extract all code to classes.
9
+
1
10
  ## App-rb 0.1.0 (March 25, 2017) ##
2
11
 
3
12
  * First public version.
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Powerfull docker apps deployer.
4
4
 
5
+ [![Build Status](https://img.shields.io/travis/uchiru/app-rb/master.svg)]()
6
+ [![Gem Version](https://img.shields.io/gem/v/app-rb.svg)]()
7
+
5
8
  ## Installation
6
9
 
7
10
  Install last gem version as:
data/exe/app-rb CHANGED
@@ -1,313 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require 'json'
3
- require 'yaml'
4
- require 'pp'
5
- require 'open3'
6
- Thread.abort_on_exception = true
7
-
8
- def yellow(txt); "\e[0;33m#{txt}\e[0m"; end
9
- def red(txt); "\e[0;31m#{txt}\e[0m"; end
10
- def green(txt); "\e[0;32m#{txt}\e[0m"; end
11
- def blue(txt); "\e[0;34m#{txt}\e[0m"; end
12
-
13
- def do_it(cmd)
14
- puts "[exec] #{cmd}"
15
- system(cmd)
16
- unless $?.success?
17
- puts red("FATAL :(")
18
- exit
19
- end
20
- end
21
-
22
- def just_cmd(cmd, skip_exit_status = false)
23
- puts "[exec] #{cmd}"
24
- output = `#{cmd}`
25
- if $?.success? || skip_exit_status
26
- output.strip
27
- else
28
- puts red(output)
29
- puts red("FATAL :(")
30
- exit
31
- end
32
- end
33
-
34
- if ARGV.count < 2
35
- puts "Just deploy your apps with docker and consul. Nothing else."
36
- puts ""
37
- puts " #{$0} <yml> <command>"
38
- puts ""
39
- puts " deploy [hash] - deploy new version of app"
40
- puts " status - status of app"
41
- puts " stop - stop app"
42
- exit
43
- end
44
-
45
- CONFIG = YAML.load(File.read(ARGV[0]))
46
- COMMAND = ARGV[1]
47
-
48
- MIN_PORT = 10_000
49
- MAX_PORT = 50_000
50
- Node = Struct.new(:name, :ip, :roles)
51
- NODES = JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/catalog/nodes")).sort_by { |n|
52
- n["Node"]
53
- }.map { |n|
54
- Node.new(n["Node"], n["Address"], (n["Meta"] || {})["roles"].to_s.split(",").reject{ |s| s.to_s.empty? })
55
- }
56
-
57
- def nodes(constraint)
58
- constraint ||= {}
59
- out = NODES
60
- if constraint["role"]
61
- out = out.select { |n| n.roles.index(constraint["role"]) }
62
- end
63
- if constraint["name"]
64
- out = out.select { |n| n.name == constraint["name"] }
65
- end
66
- out
67
- end
68
-
69
- def node(constraint)
70
- nodes(constraint).sample
71
- end
72
-
73
-
74
- def deploy(target = nil)
75
- build_node = node(CONFIG["image"]["constraint"])
76
- puts "build_node=#{build_node.to_h.inspect}"
77
-
78
- puts blue("+++ CLONE or UPDATE repository")
79
- do_it "ssh #{CONFIG["user"]}@#{build_node.ip} bash <<EOF
80
- set -e
81
- tmpfile=\\$(mktemp /tmp/git-ssh-#{CONFIG["app"]}.XXXXXX)
82
- echo '#!/bin/sh' > \\$tmpfile
83
- echo 'exec /usr/bin/ssh -o StrictHostKeyChecking=no -i #{CONFIG["image"]["key"]} \"\\$@\"' >> \\$tmpfile
84
- chmod +x \\$tmpfile
85
- export GIT_SSH=\\$tmpfile
86
- if [ -d #{CONFIG["app"]}-cache ]; then
87
- echo update cache...
88
- cd #{CONFIG["app"]}-cache
89
- git checkout . && git clean -dfx && git checkout master && git pull
90
- git branch | grep -v master | xargs -r git branch -D
91
- else
92
- echo clone...
93
- git clone git@github.com:#{CONFIG["image"]["repo"]} #{CONFIG["app"]}-cache && cd #{CONFIG["app"]}-cache
94
- fi
95
- git checkout #{target || CONFIG["image"]["target"]}
96
- rm \\$tmpfile\nEOF"
97
-
98
- puts blue("+++ calculate HASH and VERSION")
99
- hash = ARGV[2] || `ssh #{CONFIG["user"]}@#{build_node.ip} 'cd #{CONFIG["app"]}-cache && git rev-parse HEAD'`.strip
100
- puts "hash: #{hash}"
101
-
102
- o = JSON.load(`curl -s https://#{CONFIG["registry"]}/v2/#{CONFIG["app"]}/tags/list`)
103
- tags = o.is_a?(Hash) && o["errors"] ? [] : o["tags"]
104
- puts "tags: #{JSON.dump(tags)}"
105
-
106
- unless tags.index(hash)
107
- puts blue("+++ BUILD image")
108
- do_it "ssh #{CONFIG["user"]}@#{build_node.ip} bash <<EOF
109
- set -e
110
- cd #{CONFIG["app"]}-cache
111
- #{(CONFIG["image"]["pre_build"] || []).join("\n")}
112
- docker build -t #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} .
113
- docker push #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash}
114
- echo Done.\nEOF"
115
- end
116
- vv = Time.now.to_i
117
- new_service = "#{CONFIG["app"]}-#{vv}"
118
-
119
- (CONFIG["deploy"]["pre"] || []).each_with_index do |section, index|
120
- puts blue("+++ PRE: #{section.inspect}")
121
- n = node(section["constraint"] || CONFIG["deploy"]["constraint"])
122
- puts "node=#{n.inspect}"
123
- do_it "ssh #{CONFIG["user"]}@#{n.ip} docker run " +
124
- "--label app=#{CONFIG["app"]} " +
125
- "--label service=#{new_service} " +
126
- "--name #{CONFIG["app"]}-pre-#{vv}-#{index} " +
127
- "#{(CONFIG["env"] || {}).map { |k, v| "-e #{k}='#{v}'" }.join(" ")} " +
128
- "#{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} #{section["cmd"]}"
129
- do_it "ssh #{CONFIG["user"]}@#{n.ip} docker rm #{CONFIG["app"]}-pre-#{vv}-#{index}"
130
- end
131
-
132
- deploy_nodes = nodes(CONFIG["deploy"]["constraint"])
133
-
134
- puts blue("+++ PULL")
135
- pull_threads = []
136
- deploy_nodes.each do |node|
137
- pull_threads << Thread.new do
138
- host = "#{CONFIG["user"]}@#{node.ip}"
139
- Open3.popen2e("ssh #{host} docker pull #{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash}") { |i,o,w|
140
- while line = o.gets do
141
- puts "[#{node.name}] " + line
142
- end
143
- raise "FATAL" unless w.value.success?
144
- }
145
- end
146
- end
147
- pull_threads.each(&:join)
148
-
149
- plan = {}
150
- if CONFIG["deploy"]["per"]
151
- amount = CONFIG["deploy"]["per"]*deploy_nodes.count
152
- else
153
- amount = CONFIG["deploy"]["amount"]
154
- end
155
-
156
- amount.times do |index|
157
- ip = deploy_nodes[index % deploy_nodes.length].ip
158
- plan[ip] ||= []
159
- plan[ip].push(index)
160
- end
161
-
162
- puts blue("+++ DEPLOY")
163
- deploy_threads = []
164
- deploy_nodes.each do |node|
165
- deploy_threads << Thread.new do
166
- host = "#{CONFIG["user"]}@#{node.ip}"
167
-
168
- puts "[#{node.name}] run"
169
- (plan[node.ip] || []).each do |index|
170
- port = nil
171
- 10.times do
172
- a = MIN_PORT + rand(MAX_PORT - MIN_PORT)
173
- if just_cmd("ssh #{host} ss -ln src :#{a} | fgrep -c ':#{a}'", true) == "0"
174
- port = a
175
- break
176
- end
177
- end
178
- raise "Dont find free port :-((" unless port
179
- puts "[#{node.name}] port=#{port}"
180
-
181
- do_it("ssh #{host} docker run -d " +
182
- "--label app=#{CONFIG["app"]} " +
183
- "--label service=#{new_service} " +
184
- "--name=#{CONFIG["app"]}-#{vv}-#{index} " +
185
- (CONFIG["env"] || {}).map { |k, v| "-e #{k}='#{v}'" }.join(" ") + " " +
186
- "--restart unless-stopped " +
187
- "-p #{node.ip}:#{port}:#{CONFIG["deploy"]["port"]} " +
188
- "#{CONFIG["registry"]}/#{CONFIG["app"]}:#{hash} " +
189
- "#{CONFIG["deploy"]["cmd"]}")
190
-
191
- do_it %(curl -s -X PUT #{node.ip}:8500/v1/agent/service/register -d'{
192
- "Id": "#{CONFIG["app"]}-#{vv}-#{index}",
193
- "Name": "#{new_service}",
194
- "Port": #{port},
195
- "Check": {
196
- "DeregisterCriticalServiceAfter": "20m",
197
- "Interval": "7s",
198
- "HTTP": "http://#{node.ip}:#{port}#{CONFIG["deploy"]["check_url"] || "/"}"
199
- }
200
- }')
201
- end
202
-
203
- puts blue("+++ CONSUL wait for #{node.name}")
204
- loop do
205
- statuses = JSON.load(just_cmd("curl -s #{node.ip}:8500/v1/health/service/#{CONFIG["app"]}-#{vv}")).select { |s|
206
- s["Node"]["Address"] == node.ip
207
- }.flat_map { |s| s["Checks"] }.map { |c| c["Status"] }
208
- puts "#{node.name} => #{statuses.inspect}"
209
- break if statuses.uniq == ["passing"] or (plan[node.ip] || []).empty?
210
- sleep 3
211
- end
212
- end
213
- end
214
- deploy_threads.each(&:join)
215
-
216
- current_service = just_cmd("curl #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw")
217
- current_hash = just_cmd("curl #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash?raw")
218
- puts "CURRENT_SERVICE=#{current_service}"
219
- puts "CURRENT_HASH=#{current_hash}"
220
- puts "NEW_SERVICE=#{new_service}"
221
- do_it "curl -X PUT #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service -d#{new_service}"
222
- do_it "curl -X PUT #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash -d#{hash}"
223
- puts "\n\n" + green(">>>>>>>>>>>>>>> BLUE/GREEN switch <<<<<<<<<<<<<<<")
224
- sleep 3
225
-
226
- puts blue("+++ STOP OLD containers")
227
- JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/catalog/services")).keys.select { |service|
228
- service != new_service && service =~ /^#{CONFIG["app"]}-\d+$/
229
- }.each do |service|
230
- JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/health/service/#{service}")).each do |s|
231
- do_it %(curl -s -X DELETE #{s["Node"]["Address"]}:8500/v1/agent/service/deregister/#{s["Service"]["ID"]})
232
- end
233
- end
234
-
235
- NODES.each do |n|
236
- keep_ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]} -f label=service=#{new_service}").split("\n")
237
- all_ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]}").split("\n")
238
- if (all_ids - keep_ids).length > 0
239
- do_it("ssh #{CONFIG["user"]}@#{n.ip} docker stop #{(all_ids - keep_ids).join(" ")}")
240
- do_it("ssh #{CONFIG["user"]}@#{n.ip} docker rm #{(all_ids - keep_ids).join(" ")}")
241
- end
242
- end
243
-
244
- puts blue("+++ CLEAN REGISTRY")
245
- (tags - [hash, current_hash]).each do |hash|
246
- 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")
247
- puts "digest = #{digest}"
248
- system "curl -X DELETE https://#{CONFIG["registry"]}/v2/#{CONFIG["app"]}/manifests/#{digest}"
249
- end
250
-
251
- puts green("Done.")
252
- if current_hash != "" && !target
253
- puts "to rollback execute: #{$0} #{ARGV[0]} deploy #{current_hash}"
254
- end
255
- end
256
-
257
-
258
- def print_status
259
- max_name_len = NODES.map { |n| n.name.length }.max
260
- max_ip_len = NODES.map { |n| n.ip.length }.max
261
- max_roles_len = NODES.map { |n| n.roles.inspect.length }.max
262
- current_service = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw")
263
- current_hash = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/hash?raw")
264
- current_dockers = NODES.map { |n|
265
- just_cmd("ssh #{CONFIG["user"]}@#{n.ip} 'docker ps -q -f label=app=#{CONFIG["app"]} -f label=service=#{current_service} | wc -l'").to_i
266
- }
267
- dockers = NODES.map { |n|
268
- just_cmd("ssh #{CONFIG["user"]}@#{n.ip} 'docker ps -q -f label=app=#{CONFIG["app"]} | wc -l'").to_i
269
- }
270
- puts ""
271
- puts green("App: ") + CONFIG["app"]
272
- puts green("Service: ") + current_service
273
- puts green("Hash: ") + current_hash
274
- NODES.each_with_index do |n, i|
275
- puts(
276
- " "*5 + n.name.rjust(max_name_len) +
277
- " "*2 + n.ip.ljust(max_ip_len) +
278
- " "*2 + n.roles.inspect.ljust(max_roles_len) +
279
- " "*2 + green(current_dockers[i]) + " / " + (dockers[i] - current_dockers[i] == 0 ? "0" : red(dockers[i] - current_dockers[i]))
280
- )
281
- end
282
- end
283
-
284
-
285
- def do_stop
286
- current_service = just_cmd("curl -s #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}/service?raw")
287
- if current_service != ""
288
- JSON.load(just_cmd("curl -s #{CONFIG["consul"]}/v1/health/service/#{current_service}")).each do |s|
289
- do_it %(curl -s -X DELETE #{s["Node"]["Address"]}:8500/v1/agent/service/deregister/#{s["Service"]["ID"]})
290
- end
291
- end
292
- NODES.map { |n|
293
- ids = just_cmd("ssh #{CONFIG["user"]}@#{n.ip} docker ps -q -f label=app=#{CONFIG["app"]}").gsub("\n", " ")
294
- if ids != ""
295
- do_it "ssh #{CONFIG["user"]}@#{n.ip} docker stop #{ids}"
296
- do_it "ssh #{CONFIG["user"]}@#{n.ip} docker rm #{ids}"
297
- end
298
- }
299
- do_it "curl -X DELETE #{CONFIG["consul"]}/v1/kv/apps/#{CONFIG["app"]}?recurse"
300
- puts ""
301
- end
302
-
303
-
304
- if COMMAND == "deploy" || COMMAND == "d"
305
- deploy(ARGV[2])
306
- elsif COMMAND == "status" || COMMAND == "s"
307
- print_status
308
- elsif COMMAND == "stop"
309
- do_stop
310
- else
311
- puts "FATAL: unknown command '#{COMMAND}'"
312
- end
2
+ require "bundler/setup"
3
+ require "app-rb"
313
4
 
5
+ AppRb::Cli.new(ARGV).run
data/lib/app-rb/cli.rb CHANGED
@@ -1,4 +1,52 @@
1
+ require 'yaml'
2
+
1
3
  module AppRb
2
4
  class Cli
5
+ def initialize(args)
6
+ @args = args
7
+ Thread.abort_on_exception = true
8
+ end
9
+
10
+ def run
11
+ if @args.count < 2
12
+ usage
13
+ exit
14
+ end
15
+ config = Config.new(YAML.load(File.read(@args[0])))
16
+ command = @args[1]
17
+
18
+ if AppRb::Util.compare_versions(config.tool_version, AppRb::VERSION) > 0
19
+ puts "FATAL: need at least '#{config.tool_version}' tool version but current version is '#{AppRb::VERSION}'"
20
+ exit -1
21
+ end
22
+
23
+ if command == "deploy" || command == "d"
24
+ Command.new(config).deploy(@args[2])
25
+ elsif command == "status" || command == "s"
26
+ Command.new(config).status
27
+ elsif command == "restart"
28
+ Command.new(config).restart
29
+ elsif command == "clean"
30
+ Command.new(config).clean
31
+ elsif command == "stop"
32
+ Command.new(config).stop
33
+ else
34
+ puts "FATAL: unknown command '#{command}'"
35
+ exit -1
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def usage
42
+ puts "Just deploy your apps with docker and consul. Nothing else."
43
+ puts "Version: #{AppRb::VERSION}"
44
+ puts ""
45
+ puts " app-rb <yml> <command>"
46
+ puts ""
47
+ puts " deploy [hash] - deploy new version of app"
48
+ puts " status - status of app"
49
+ puts " stop - stop app"
50
+ end
3
51
  end
4
52
  end
@@ -0,0 +1,232 @@
1
+ module AppRb
2
+ class Command
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def deploy(target)
8
+ @base = "#{@config.app}-#{Time.now.to_i}"
9
+
10
+ # init
11
+ current_hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
12
+ build_nodes = @config.nodes(@config.image["constraint"])
13
+ pre_payloads = @config.pre_deploy.map do |pre|
14
+ {
15
+ "nodes" => @config.nodes(pre["constraint"]),
16
+ "cmd" => pre["cmd"],
17
+ "opts" => pre["opts"] || [],
18
+ }
19
+ end
20
+ deploy_payloads = @config.deploy.map { |key, section|
21
+ nodes = @config.nodes(section["constraint"])
22
+ {
23
+ "key" => key,
24
+ "nodes" => nodes,
25
+ "amount" => (section["per"] ? section["per"]*nodes.count : section["amount"]),
26
+ "cmd" => section["cmd"],
27
+ "port" => section["port"],
28
+ "check_url" => section["check_url"],
29
+ "opts" => section["opts"] || [],
30
+ }
31
+ }
32
+ old_ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
33
+ new_ips = (
34
+ build_nodes.map(&:ip) +
35
+ pre_payloads.flat_map { |p| p["nodes"].map(&:ip) } +
36
+ deploy_payloads.flat_map { |p| p["nodes"].map(&:ip) }
37
+ ).uniq
38
+ ips = (old_ips + new_ips).uniq
39
+ AppRb::Util::Consul.kv_set(@config.consul, @config.app, "nodes", ips.join(","))
40
+
41
+ # pre
42
+ new_hash = prepare_image(build_nodes, target)
43
+ pre_deploy(pre_payloads, new_hash)
44
+ stop_bg_jobs(ips)
45
+
46
+ # deploy
47
+ do_deploy(deploy_payloads, new_hash)
48
+
49
+ # switch
50
+ blue_green(deploy_payloads, new_hash)
51
+
52
+ # clean
53
+ stop_services(ips)
54
+ clean_registry(current_hash, [current_hash, new_hash].uniq)
55
+
56
+ # finish
57
+ AppRb::Util::Consul.kv_set(@config.consul, @config.app, "nodes", new_ips.join(","))
58
+ puts AppRb::Util.green("Done.")
59
+ if current_hash != "" && !target && current_hash != target
60
+ puts "to rollback fire: app-rb #{ARGV[0]} deploy #{current_hash}"
61
+ end
62
+ end
63
+
64
+ def status
65
+ ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
66
+ nodes = @config.nodes.select { |n| ips.index(n.ip) }
67
+ max_name_len = nodes.map { |n| n.name.length }.max
68
+ max_ip_len = nodes.map { |n| n.ip.length }.max
69
+ max_roles_len = nodes.map { |n| n.roles.inspect.length }.max
70
+ current_base = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "base")
71
+ current_hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
72
+ current_dockers = nodes.map { |n|
73
+ AppRb::Util::Docker.ids(@config.user, n.ip, {app: @config.app, build: current_base}).count
74
+ }
75
+ dockers = nodes.map { |n|
76
+ AppRb::Util::Docker.ids(@config.user, n.ip, {app: @config.app}).count
77
+ }
78
+ puts ""
79
+ puts AppRb::Util.green("App: ") + @config.app
80
+ puts AppRb::Util.green("Base: ") + current_base
81
+ puts AppRb::Util.green("Hash: ") + current_hash
82
+ nodes.each_with_index do |n, i|
83
+ puts(
84
+ " "*5 + n.name.rjust(max_name_len) +
85
+ " "*2 + n.ip.ljust(max_ip_len) +
86
+ " "*2 + n.roles.inspect.ljust(max_roles_len) +
87
+ " "*2 + AppRb::Util.green(current_dockers[i]) + " / " + (dockers[i] - current_dockers[i] == 0 ? "0" : AppRb::Util.red(dockers[i] - current_dockers[i]))
88
+ )
89
+ end
90
+ end
91
+
92
+ def stop
93
+ ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
94
+ stop_all(ips)
95
+ end
96
+
97
+ def clean
98
+ base = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "base")
99
+ ips = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "nodes").split(",")
100
+ stop_services(ips, base)
101
+ end
102
+
103
+ def restart
104
+ hash = AppRb::Util::Consul.kv_get(@config.consul, @config.app, "hash")
105
+ raise "FATAL: app is not started?" if hash == ""
106
+ puts "hash=#{hash}"
107
+ deploy(hash)
108
+ end
109
+
110
+ private
111
+
112
+ def full_image_name(hash)
113
+ "#{@config.registry}/#{@config.app}:#{hash}"
114
+ end
115
+
116
+ def prepare_image(build_nodes, target)
117
+ puts AppRb::Util.blue("+++ CLONE or UPDATE repository")
118
+ AppRb::Util::Build.build(
119
+ @config.user, build_nodes.sample.ip,
120
+ @config.image["repo"], @config.image["key"], target || @config.image["target"],
121
+ @config.registry, @config.app, @config.image["pre_build"] || []
122
+ )
123
+ end
124
+
125
+ def pre_deploy(pre_payloads, hash)
126
+ pre_payloads.each_with_index do |payload, index|
127
+ puts AppRb::Util.blue("+++ PRE: #{payload["cmd"].inspect}")
128
+ AppRb::Util::Docker.run_batch(
129
+ @config.user, payload["nodes"].sample.ip,
130
+ "#{@base}-pre-#{index}", full_image_name(hash), payload["cmd"],
131
+ {app: @config.app, build: @base},
132
+ @config.env,
133
+ payload["opts"]
134
+ )
135
+ end
136
+ end
137
+
138
+ def do_deploy(deploy_payloads, hash)
139
+ puts AppRb::Util.blue("+++ PULL")
140
+ deploy_payloads.flat_map { |p| p["nodes"] }.uniq.map { |node|
141
+ Thread.new do
142
+ AppRb::Util::Docker.pull(@config.user, node.ip, full_image_name(hash))
143
+ end
144
+ }.each(&:join)
145
+
146
+ deploy_payloads.each do |payload|
147
+ puts AppRb::Util.blue("+++ DEPLOY '#{payload["key"]}'")
148
+
149
+ # naive scheduling
150
+ plan = payload["nodes"].map{ |n| [n.ip, []] }.to_h
151
+ payload["amount"].times do |index|
152
+ ip = payload["nodes"][index % payload["nodes"].length].ip
153
+ plan[ip].push("#{payload["key"]}-#{index}")
154
+ end
155
+
156
+ payload["nodes"].map { |node|
157
+ Thread.new do
158
+ (plan[node.ip] || []).each do |name|
159
+ port = AppRb::Util.get_free_port(@config.user, node.ip)
160
+ puts "[#{node.name}] port=#{port}"
161
+
162
+ AppRb::Util::Docker.run_daemon(
163
+ @config.user, node.ip,
164
+ "#{@base}-#{name}", full_image_name(hash), payload["cmd"],
165
+ {app: @config.app, build: @base, has_port: (payload["port"] ? "yes" : "no")},
166
+ @config.env,
167
+ payload["opts"],
168
+ (payload["port"] ? {"#{node.ip}:#{port}" => payload["port"]} : {})
169
+ )
170
+
171
+ if payload["port"]
172
+ AppRb::Util::Consul.register_service(
173
+ node.ip,
174
+ "#{@base}-#{name}", "#{@base}-#{payload["key"]}", port, payload["check_url"] || "/",
175
+ [@config.app, @base]
176
+ )
177
+ end
178
+ end
179
+ end
180
+ }.each(&:join)
181
+
182
+ if payload["port"]
183
+ puts AppRb::Util.blue("+++ CONSUL wait '#{payload["key"]}'")
184
+ AppRb::Util::Consul.consul_wait(@config.consul, "#{@base}-#{payload["key"]}")
185
+ end
186
+ end
187
+ end
188
+
189
+ def blue_green(deploy_payloads, new_hash)
190
+ service_payloads = deploy_payloads.select { |p| p["port"] }
191
+ AppRb::Util::Consul.kv_set(@config.consul, @config.app, "hash", new_hash)
192
+ AppRb::Util::Consul.kv_set(@config.consul, @config.app, "base", @base)
193
+ service_payloads.each do |payload|
194
+ AppRb::Util::Consul.kv_set(@config.consul, @config.app, "services/#{payload["key"]}", "#{@base}-#{payload["key"]}")
195
+ end
196
+ puts "\n" + AppRb::Util.green(">>>>>>>>>>>>>>> BLUE/GREEN switch <<<<<<<<<<<<<<<") + "\n\n"
197
+ sleep 3
198
+ (AppRb::Util::Consul.kv_keys(@config.consul, @config.app + "/services") - service_payloads.map { |p| p["key"] }).each do |remove|
199
+ AppRb::Util::Consul.kv_unset(@config.consul, @config.app + "/services/#{remove}")
200
+ end
201
+ end
202
+
203
+ def stop_bg_jobs(ips)
204
+ puts AppRb::Util.blue("+++ STOP OLD backgroud jobs")
205
+ ips.each do |ip|
206
+ AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app, has_port: "no"})
207
+ end
208
+ end
209
+
210
+ def stop_services(ips, base = @base)
211
+ puts AppRb::Util.blue("+++ STOP services")
212
+ AppRb::Util::Consul.remove_services(@config.consul, [@config.app], base)
213
+ ips.each do |ip|
214
+ AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app, has_port: "yes"}, {build: base})
215
+ end
216
+ end
217
+
218
+ def stop_all(ips)
219
+ puts AppRb::Util.blue("+++ STOP")
220
+ AppRb::Util::Consul.remove_services(@config.consul, [@config.app])
221
+ ips.each do |ip|
222
+ AppRb::Util::Docker.stop(@config.user, ip, {app: @config.app})
223
+ end
224
+ AppRb::Util::Consul.kv_unset(@config.consul, @config.app)
225
+ end
226
+
227
+ def clean_registry(current_hash, keep_hashes = [])
228
+ puts AppRb::Util.blue("+++ CLEAN REGISTRY")
229
+ AppRb::Util::Registry.clean(@config.registry, @config.app, keep_hashes)
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,38 @@
1
+ class AppRb::Config
2
+ def initialize(yml)
3
+ @body = yml
4
+ end
5
+
6
+ def tool_version; @body["tool_version"]; end
7
+ def app; @body["app"]; end
8
+ def consul; @body["consul"]; end
9
+ def registry; @body["registry"]; end
10
+ def user; @body["user"]; end
11
+ def image; @body["image"]; end
12
+ def env; @body["env"] || {}; end
13
+ def pre_deploy; @body["pre_deploy"] || []; end
14
+ def deploy; @body["deploy"] || {}; end
15
+
16
+ def nodes(constraint = nil)
17
+ constraint ||= {}
18
+ out = __nodes
19
+ if constraint["role"]
20
+ out = out.select { |n| n.roles.index(constraint["role"]) }
21
+ end
22
+ if constraint["name"]
23
+ out = out.select { |n| n.name == constraint["name"] }
24
+ end
25
+ out
26
+ end
27
+
28
+ private
29
+
30
+ Node = Struct.new(:name, :ip, :roles)
31
+ def __nodes
32
+ @__nodes ||= JSON.load(AppRb::Util.just_cmd("curl -s #{@body["consul"]}/v1/catalog/nodes")).sort_by { |n|
33
+ n["Node"]
34
+ }.map { |n|
35
+ Node.new(n["Node"], n["Address"], (n["Meta"] || {})["roles"].to_s.split(",").reject{ |s| s.to_s.empty? })
36
+ }
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ module AppRb::Util::Build
2
+ def self.build(user, host, repo, ssh_key, target, registry, image_name, pre_build_cmds = [])
3
+ AppRb::Util.do_it "ssh #{user}@#{host} bash <<EOF
4
+ set -e
5
+ tmpfile=\\$(mktemp /tmp/git-ssh-#{repo_cache(repo)}.XXXXXX)
6
+ echo '#!/bin/sh' > \\$tmpfile
7
+ echo 'exec /usr/bin/ssh -o StrictHostKeyChecking=no -i #{ssh_key} \"\\$@\"' >> \\$tmpfile
8
+ chmod +x \\$tmpfile
9
+ export GIT_SSH=\\$tmpfile
10
+ if [ -d #{repo_cache(repo)} ]; then
11
+ echo update cache...
12
+ cd #{repo_cache(repo)}
13
+ git checkout . && git clean -dfx && git checkout master && git pull
14
+ git branch | grep -v master | xargs -r git branch -D
15
+ else
16
+ echo clone...
17
+ git clone git@github.com:#{repo} #{repo_cache(repo)} && cd #{repo_cache(repo)}
18
+ fi
19
+ git checkout #{target}
20
+ rm \\$tmpfile\nEOF"
21
+
22
+ hash = AppRb::Util.just_cmd("ssh #{user}@#{host} 'cd #{repo_cache(repo)} && git rev-parse HEAD'")
23
+ tags = AppRb::Util::Registry.tags_list(registry, image_name)
24
+ puts "hash: #{hash}"
25
+ puts "tags: #{tags.inspect}"
26
+
27
+ unless tags.index(hash)
28
+ puts AppRb::Util.blue("+++ BUILD image")
29
+ AppRb::Util.do_it "ssh #{user}@#{host} bash <<EOF
30
+ set -e
31
+ cd #{repo_cache(repo)}
32
+ #{pre_build_cmds.join("\n")}
33
+ docker build -t #{registry}/#{image_name}:#{hash} .
34
+ docker push #{registry}/#{image_name}:#{hash}
35
+ echo Done.\nEOF"
36
+ end
37
+
38
+ return hash
39
+ end
40
+
41
+ private
42
+
43
+ def self.repo_cache(repo)
44
+ repo.gsub("/", "-") + "-cache"
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ module AppRb::Util::Consul
2
+ def self.register_service(host, id, name, port, url, tags = [])
3
+ AppRb::Util.do_it %(curl -s -X PUT #{host}:8500/v1/agent/service/register -d'{
4
+ "Id": "#{id}",
5
+ "Name": "#{name}",
6
+ "Port": #{port},
7
+ "Tags": #{JSON.dump(tags)},
8
+ "Check": {
9
+ "DeregisterCriticalServiceAfter": "20m",
10
+ "Interval": "7s",
11
+ "HTTP": "http://#{host}:#{port}#{url}"
12
+ }
13
+ }')
14
+ end
15
+
16
+ def self.remove_services(consul, tags = [], except = nil)
17
+ JSON.load(AppRb::Util.just_cmd("curl -s #{consul}/v1/catalog/services")).select { |k, v|
18
+ (v & tags) == tags
19
+ }.keys.each do |service|
20
+ JSON.load(AppRb::Util.just_cmd("curl -s #{consul}/v1/catalog/service/#{service}")).each do |s|
21
+ if except && s["ServiceTags"].index(except)
22
+ # keep this service
23
+ else
24
+ AppRb::Util.do_it %(curl -s -X DELETE #{s["Address"]}:8500/v1/agent/service/deregister/#{s["ServiceID"]})
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.consul_wait(consul, service)
31
+ loop do
32
+ url = "curl -s #{consul}/v1/health/service/#{service}"
33
+ statuses = JSON.load(AppRb::Util.just_cmd(url)).flat_map { |s|
34
+ s["Checks"]
35
+ }.reject { |c|
36
+ c["CheckID"] == "serfHealth"
37
+ }.map { |c|
38
+ c["Status"]
39
+ }
40
+ puts "statuses: #{statuses.inspect}"
41
+ break if statuses.uniq == ["passing"] || statuses.uniq == []
42
+ sleep 3
43
+ end
44
+ end
45
+
46
+ CONSUL_DIR = "apps"
47
+
48
+ def self.kv_get(consul, relative_path, k)
49
+ AppRb::Util.just_cmd "curl -s #{consul}/v1/kv/#{CONSUL_DIR}/#{relative_path}/#{k}?raw"
50
+ end
51
+
52
+ def self.kv_set(consul, relative_path, k, v)
53
+ AppRb::Util.do_it "curl -s -X PUT #{consul}/v1/kv/#{CONSUL_DIR}/#{relative_path}/#{k} -d#{v}"
54
+ puts ""
55
+ end
56
+
57
+ def self.kv_unset(consul, relative_path)
58
+ AppRb::Util.do_it "curl -s -X DELETE #{consul}/v1/kv/#{CONSUL_DIR}/#{relative_path}?recurse"
59
+ puts ""
60
+ end
61
+
62
+ def self.kv_keys(consul, relative_path)
63
+ (JSON.load(AppRb::Util.just_cmd "curl -s #{consul}/v1/kv/#{CONSUL_DIR}/#{relative_path}?recurse") || []).map { |v|
64
+ v["Key"].sub("#{CONSUL_DIR}/#{relative_path}/", "")
65
+ }
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ require 'open3'
2
+
3
+ module AppRb::Util::Docker
4
+ def self.pull(user, host, image)
5
+ Open3.popen2e("ssh #{user}@#{host} docker pull #{image}") { |i,o,w|
6
+ while line = o.gets do
7
+ puts "[#{host}] " + line
8
+ end
9
+ raise "FATAL" unless w.value.success?
10
+ }
11
+ end
12
+
13
+ def self.run_batch(user, host, name, image, cmd, labels = {}, env = {}, opts = [])
14
+ AppRb::Util.do_it "ssh #{user}@#{host} docker run " +
15
+ labels.map { |k, v| "--label #{k}=#{v} " }.join +
16
+ "--name #{name} " +
17
+ opts.join(" ") + " " +
18
+ env.map { |k, v| "-e #{k}='#{v}' " }.join +
19
+ "#{image} #{cmd}"
20
+ AppRb::Util.do_it "ssh #{user}@#{host} docker rm #{name}"
21
+ end
22
+
23
+ def self.run_daemon(user, host, name, image, cmd, labels = {}, env = {}, opts = [], ports = {})
24
+ AppRb::Util.do_it "ssh #{user}@#{host} docker run -d " +
25
+ "--restart unless-stopped " +
26
+ labels.map { |k, v| "--label #{k}=#{v} " }.join +
27
+ "--name #{name} " +
28
+ opts.join(" ") + " " +
29
+ env.map { |k, v| "-e #{k}='#{v}' " }.join +
30
+ ports.map { |k, v| "-p #{k}:#{v} " }.join +
31
+ "#{image} #{cmd}"
32
+ end
33
+
34
+ def self.ids(user, host, labels = {})
35
+ filters = labels.map { |k, v| "-f label=#{k}=#{v} " }.join("")
36
+ AppRb::Util.just_cmd("ssh #{user}@#{host} docker ps -q #{filters}").split("\n")
37
+ end
38
+
39
+ def self.stop(user, host, labels = {}, except = nil)
40
+ keep_ids = except ? ids(user, host, labels.merge(except)) : []
41
+ all_ids = ids(user, host, labels)
42
+ if (all_ids - keep_ids).length > 0
43
+ AppRb::Util.do_it("ssh #{user}@#{host} docker stop #{(all_ids - keep_ids).join(" ")}")
44
+ AppRb::Util.do_it("ssh #{user}@#{host} docker rm #{(all_ids - keep_ids).join(" ")}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ module AppRb::Util::Registry
2
+ def self.tags_list(registry, image_name)
3
+ o = JSON.load(`curl -s https://#{registry}/v2/#{image_name}/tags/list`)
4
+ o.is_a?(Hash) && o["errors"] ? [] : o["tags"]
5
+ end
6
+
7
+ def self.clean(registry, image_name, keep_tags = [])
8
+ (tags_list(registry, image_name) - keep_tags).each do |hash|
9
+ digest = AppRb::Util.just_cmd("curl -s --head -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' https://#{registry}/v2/#{image_name}/manifests/#{hash} | grep Docker-Content-Digest | cut -d' ' -f2")
10
+ AppRb::Util.do_it "curl -s -X DELETE https://#{registry}/v2/#{image_name}/manifests/#{digest}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module AppRb::Util
2
+ MIN_PORT = 10_000
3
+ MAX_PORT = 50_000
4
+
5
+ def self.yellow(txt); "\e[0;33m#{txt}\e[0m"; end
6
+ def self.red(txt); "\e[0;31m#{txt}\e[0m"; end
7
+ def self.green(txt); "\e[0;32m#{txt}\e[0m"; end
8
+ def self.blue(txt); "\e[0;34m#{txt}\e[0m"; end
9
+
10
+ def self.do_it(cmd)
11
+ puts "[exec] #{cmd}"
12
+ system(cmd)
13
+ unless $?.success?
14
+ puts red("FATAL :(")
15
+ exit
16
+ end
17
+ end
18
+
19
+ def self.just_cmd(cmd, skip_exit_status = false)
20
+ puts "[exec] #{cmd}"
21
+ output = `#{cmd}`
22
+ if $?.success? || skip_exit_status
23
+ output.strip
24
+ else
25
+ puts red(output)
26
+ puts red("FATAL :(")
27
+ exit
28
+ end
29
+ end
30
+
31
+ def self.compare_versions(a, b)
32
+ parse = proc { |v| v.split(".", 3).map(&:to_i) + [v.index("-dev") ? 1 : 0] }
33
+ parse.call(a) <=> parse.call(b)
34
+ end
35
+
36
+ def self.get_free_port(user, host)
37
+ port = nil
38
+ 10.times do
39
+ a = MIN_PORT + rand(MAX_PORT - MIN_PORT)
40
+ if just_cmd("ssh #{user}@#{host} ss -ln src :#{a} | fgrep -c ':#{a}'", true) == "0"
41
+ port = a
42
+ break
43
+ end
44
+ end
45
+ raise "Dont find free port :-((" unless port
46
+ port
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module AppRb
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/app-rb.rb CHANGED
@@ -1,5 +1,14 @@
1
- require "app-rb/version"
1
+ require "pp"
2
+ require 'json'
2
3
  require "app-rb/cli"
4
+ require "app-rb/version"
5
+ require "app-rb/command"
6
+ require "app-rb/config"
7
+ require "app-rb/util"
8
+ require "app-rb/util/build"
9
+ require "app-rb/util/consul"
10
+ require "app-rb/util/docker"
11
+ require "app-rb/util/registry"
3
12
 
4
13
  module AppRb
5
14
  def self.get_version
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: app-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Vakhov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-24 00:00:00.000000000 Z
11
+ date: 2017-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,6 +73,13 @@ files:
73
73
  - exe/app-rb
74
74
  - lib/app-rb.rb
75
75
  - lib/app-rb/cli.rb
76
+ - lib/app-rb/command.rb
77
+ - lib/app-rb/config.rb
78
+ - lib/app-rb/util.rb
79
+ - lib/app-rb/util/build.rb
80
+ - lib/app-rb/util/consul.rb
81
+ - lib/app-rb/util/docker.rb
82
+ - lib/app-rb/util/registry.rb
76
83
  - lib/app-rb/version.rb
77
84
  homepage: https://github.com/uchiru/app-rb
78
85
  licenses: