app-rb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: