mamiya 0.0.1.alpha24 → 0.0.1.beta1

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -18
  3. data/example/config.agent.rb +3 -0
  4. data/example/config.rb +27 -0
  5. data/example/deploy.rb +19 -11
  6. data/lib/mamiya/agent.rb +38 -2
  7. data/lib/mamiya/agent/actions.rb +4 -0
  8. data/lib/mamiya/agent/tasks/clean.rb +35 -0
  9. data/lib/mamiya/agent/tasks/notifyable.rb +4 -1
  10. data/lib/mamiya/agent/tasks/prepare.rb +2 -0
  11. data/lib/mamiya/agent/tasks/switch.rb +123 -0
  12. data/lib/mamiya/cli.rb +1 -20
  13. data/lib/mamiya/cli/client.rb +225 -3
  14. data/lib/mamiya/configuration.rb +18 -0
  15. data/lib/mamiya/master/agent_monitor.rb +19 -4
  16. data/lib/mamiya/master/agent_monitor_handlers.rb +10 -0
  17. data/lib/mamiya/master/application_status.rb +72 -0
  18. data/lib/mamiya/master/package_status.rb +178 -0
  19. data/lib/mamiya/master/web.rb +48 -44
  20. data/lib/mamiya/steps/build.rb +1 -1
  21. data/lib/mamiya/steps/prepare.rb +1 -1
  22. data/lib/mamiya/steps/switch.rb +2 -1
  23. data/lib/mamiya/storages/filesystem.rb +1 -1
  24. data/lib/mamiya/storages/mock.rb +1 -1
  25. data/lib/mamiya/storages/s3.rb +1 -1
  26. data/lib/mamiya/storages/s3_proxy.rb +1 -1
  27. data/lib/mamiya/version.rb +1 -1
  28. data/spec/agent/actions_spec.rb +26 -1
  29. data/spec/agent/tasks/clean_spec.rb +88 -2
  30. data/spec/agent/tasks/notifyable_spec.rb +3 -3
  31. data/spec/agent/tasks/switch_spec.rb +176 -0
  32. data/spec/agent_spec.rb +142 -1
  33. data/spec/configuration_spec.rb +23 -0
  34. data/spec/master/agent_monitor_spec.rb +128 -38
  35. data/spec/master/application_status_spec.rb +171 -0
  36. data/spec/master/package_status_spec.rb +560 -0
  37. data/spec/master/web_spec.rb +116 -1
  38. data/spec/steps/build_spec.rb +1 -1
  39. data/spec/steps/prepare_spec.rb +6 -1
  40. data/spec/steps/switch_spec.rb +6 -2
  41. data/spec/storages/filesystem_spec.rb +2 -21
  42. data/spec/storages/s3_proxy_spec.rb +2 -22
  43. data/spec/storages/s3_spec.rb +2 -20
  44. metadata +11 -2
data/lib/mamiya/cli.rb CHANGED
@@ -47,11 +47,6 @@ module Mamiya
47
47
  end
48
48
  end
49
49
 
50
- desc "status", "Show status of servers"
51
- def status
52
- # TODO:
53
- end
54
-
55
50
  desc "list-packages", "List packages in storage"
56
51
  method_option :name_only, aliases: '-n'
57
52
  def list_packages
@@ -88,14 +83,6 @@ module Mamiya
88
83
 
89
84
  # ---
90
85
 
91
- desc "deploy PACKAGE", "Run build->push->distribute->prepare->finalize"
92
- def deploy
93
- end
94
-
95
- desc "rollback", "Switch back to previous release then finalize"
96
- def rollback
97
- end
98
-
99
86
  desc "build", "Build package."
100
87
  method_option :build_from, aliases: %w(--source -f), type: :string
101
88
  method_option :build_to, aliases: %w(--destination -t), type: :string
@@ -216,12 +203,6 @@ module Mamiya
216
203
  @agent.run!
217
204
  end
218
205
 
219
- # def worker
220
- # end
221
-
222
- # def event_handler
223
- # end
224
-
225
206
  private
226
207
 
227
208
  def prepare_agent_behavior!
@@ -247,7 +228,7 @@ module Mamiya
247
228
 
248
229
  def config(dont_raise_error = false)
249
230
  return @config if @config
250
- path = [options[:config], './mamiya.conf.rb', './config.rb'].compact.find { |_| File.exists?(_) }
231
+ path = [options[:config], './mamiya.conf.rb', './config.rb', '/etc/mamiya/config.rb'].compact.find { |_| File.exists?(_) }
251
232
 
252
233
  if path
253
234
  logger.debug "Using configuration: #{path}"
@@ -28,7 +28,7 @@ module Mamiya
28
28
  desc "show-package", "show package meta data"
29
29
  method_option :format, aliases: %w(-f), type: :string, default: 'pp'
30
30
  def show_package(package)
31
- meta = master_get("/packages/#{application}/#{package}")
31
+ meta = @meta = master_get("/packages/#{application}/#{package}")
32
32
 
33
33
  case options[:format]
34
34
  when 'pp'
@@ -77,6 +77,7 @@ module Mamiya
77
77
  end
78
78
  end
79
79
 
80
+ # TODO: Deprecated. Remove this
80
81
  desc "show-distribution package", "Show package distribution status"
81
82
  method_option :format, aliases: %w(-f), type: :string, default: 'text'
82
83
  method_option :verbose, aliases: %w(-v), type: :boolean
@@ -127,6 +128,19 @@ not distributed: #{dist['not_distributed_count']} agents
127
128
  end
128
129
  end
129
130
 
131
+ desc "status [PACKAGE]", "Show application or package status"
132
+ method_option :format, aliases: %w(-f), type: :string, default: 'text'
133
+ method_option :labels, type: :string
134
+ method_option :show_done, type: :boolean, default: false
135
+ def status(package=nil)
136
+ if package
137
+ pkg_status package
138
+ else
139
+ app_status
140
+ end
141
+ end
142
+
143
+
130
144
  desc "distribute package", "order distributing package to agents"
131
145
  method_option :labels, type: :string
132
146
  def distribute(package)
@@ -145,17 +159,136 @@ not distributed: #{dist['not_distributed_count']} agents
145
159
  p master_post("/packages/#{application}/#{package}/prepare", params.merge(type: :json))
146
160
  end
147
161
 
162
+ desc "switch PACKAGE", "order switching package to agents"
163
+ method_option :labels, type: :string
164
+ method_option :no_release, type: :boolean, default: false
165
+ def switch(package)
166
+ params = {no_release: options[:no_release]}
167
+ if options[:labels]
168
+ params[:labels] = Mamiya::Util::LabelMatcher.parse_string_expr(options[:labels])
169
+ end
170
+
171
+ p master_post("/packages/#{application}/#{package}/switch", params.merge(type: :json))
172
+ end
173
+
148
174
  desc "refresh", "order refreshing agent status"
149
175
  def refresh
150
176
  p master_post('/agents/refresh')
151
177
  end
152
178
 
153
- desc "deploy PACKAGE", "Run distribute->prepare->finalize"
154
- def deploy
179
+ desc "deploy PACKAGE", "Prepare, then switch"
180
+ method_option :labels, type: :string
181
+ method_option :no_release, type: :boolean, default: false
182
+ method_option :config, aliases: '-C', type: :string
183
+ method_option :no_switch, type: :boolean, default: false
184
+ def deploy(package)
185
+ @deploy_exception = nil
186
+ # TODO: move this run on master node side
187
+ puts "=> Deploying #{application}/#{package}"
188
+ puts " * with labels: #{options[:labels].inspect}" if options[:labels] && !options[:labels].empty?
189
+
190
+ show_package(package)
191
+
192
+ if config
193
+ config.set :application, application
194
+ config.set :package_name, package
195
+ config.set :package, @meta
196
+
197
+ config.before_deploy_or_rollback[]
198
+ config.before_deploy[]
199
+ end
200
+
201
+ do_prep = -> do
202
+ puts "=> Preparing..."
203
+ prepare(package)
204
+ end
205
+
206
+ do_prep[]
207
+
208
+ puts " * Wait until prepared"
209
+ puts ""
210
+
211
+ i = 0
212
+ loop do
213
+ i += 1
214
+ do_prep[] if i % 25 == 0
215
+
216
+ s = pkg_status(package, :short)
217
+ puts ""
218
+ break if 0 < s['participants_count'] && s['participants_count'] == s['prepare']['done'].size
219
+ sleep 2
220
+ end
221
+
222
+ ###
223
+
224
+ unless options[:no_switch]
225
+ puts "=> Switching..."
226
+ switch(package)
227
+
228
+ puts " * Wait until switch"
229
+ puts ""
230
+ loop do
231
+ s = pkg_status(package, :short)
232
+ puts ""
233
+ break if s['participants_count'] == s['switch']['done'].size
234
+ sleep 2
235
+ end
236
+ end
237
+ rescue Exception => e
238
+ @deploy_exception = e
239
+ $stderr.puts "ERROR: #{e.inspect}"
240
+ $stderr.puts "\t#{e.backtrace("\n\t")}"
241
+ ensure
242
+ config.after_deploy[@deploy_exception] if config
243
+ config.after_deploy_or_rollback[@deploy_exception] if config
244
+ puts "=> Done."
155
245
  end
156
246
 
157
247
  desc "rollback", "Switch back to previous release then finalize"
248
+ method_option :labels, type: :string
249
+ method_option :no_release, type: :boolean, default: false
250
+ method_option :config, aliases: '-C', type: :string
158
251
  def rollback
252
+ @deploy_exception = nil
253
+ # TODO: move this run on master node side
254
+ appstatus = master_get("/applications/#{application}/status", options[:labels] ? {labels: options[:labels]} : {})
255
+ package = appstatus['common_previous_release']
256
+
257
+ unless package
258
+ raise 'there is no common_previous_release for specified application'
259
+ end
260
+
261
+ puts "=> Rolling back #{application} to #{package}"
262
+ puts " * with labels: #{options[:labels].inspect}" if options[:labels] && !options[:labels].empty?
263
+
264
+ show_package(package)
265
+
266
+ if config
267
+ config.set :application, application
268
+ config.set :package_name, package
269
+ config.set :package, @meta
270
+
271
+ config.before_deploy_or_rollback[]
272
+ config.before_rollback[]
273
+ end
274
+
275
+ switch(package)
276
+
277
+ puts " * Wait until switch"
278
+ puts ""
279
+ loop do
280
+ s = pkg_status(package, :short)
281
+ puts ""
282
+ break if 0 < s['participants_count'] && s['participants_count'] == s['switch']['done'].size
283
+ sleep 2
284
+ end
285
+ rescue Exception => e
286
+ @deploy_exception = e
287
+ raise e
288
+ ensure
289
+ config.after_rollback[@deploy_exception] if config
290
+ config.after_deploy_or_rollback[@deploy_exception] if config
291
+ puts "=> Done."
159
292
  end
160
293
 
161
294
  desc "join HOST", "let serf to join to HOST"
@@ -174,6 +307,17 @@ not distributed: #{dist['not_distributed_count']} agents
174
307
  options[:application] or fatal!('specify application')
175
308
  end
176
309
 
310
+ def config
311
+ return @config if @config
312
+ path = [options[:config], './mamiya.conf.rb', './config.rb', '/etc/mamiya/config.rb'].compact.find { |_| File.exists?(_) }
313
+
314
+ if path
315
+ @config = Mamiya::Configuration.new.load!(File.expand_path(path))
316
+ end
317
+
318
+ @config
319
+ end
320
+
177
321
  def master_get(path, params={})
178
322
  path += "?#{Rack::Utils.build_query(params)}" unless params.empty?
179
323
  master_http.start do |http|
@@ -217,6 +361,84 @@ not distributed: #{dist['not_distributed_count']} agents
217
361
  fatal! 'specify master URL via --master(-u) option or $MAMIYA_MASTER_URL' unless url
218
362
  URI.parse(url)
219
363
  end
364
+
365
+ def app_status
366
+ params = options[:labels] ? {labels: options[:labels]} : {}
367
+ status = master_get("/applications/#{application}/status", params)
368
+
369
+ case options[:format]
370
+ when 'json'
371
+ require 'json'
372
+ puts status.to_json
373
+ return
374
+
375
+ when 'yaml'
376
+ require 'yaml'
377
+ puts status.to_yaml
378
+ return
379
+
380
+ end
381
+
382
+ puts <<-EOF
383
+ at: #{Time.now.inspect}
384
+ application: #{application}
385
+ participants: #{status['participants_count']} agents
386
+
387
+ major_current: #{status['major_current']}
388
+ currents:
389
+ #{status['currents'].sort_by { |pkg, as| -(as.size) }.flat_map { |pkg, as|
390
+ [" - #{pkg} (#{as.size} agents)"] + (pkg == status['major_current'] ? [" + (omitted)"] : as.map{ |_| " * #{_['name']}" })
391
+ }.join("\n")}
392
+
393
+ common_previous_release: #{status['common_previous_release']}
394
+ common_releases:
395
+ #{status['common_releases'].map { |_| _.prepend(' - ') }.join("\n")}
396
+ EOF
397
+ end
398
+
399
+ def pkg_status(package, short=false)
400
+ params = options[:labels] ? {labels: options[:labels]} : {}
401
+ status = master_get("/packages/#{application}/#{package}/status", params)
402
+
403
+ case options[:format]
404
+ when 'json'
405
+ require 'json'
406
+ puts status.to_json
407
+ return
408
+
409
+ when 'yaml'
410
+ require 'yaml'
411
+ puts status.to_yaml
412
+ return
413
+
414
+ end
415
+
416
+ total = status['participants_count']
417
+
418
+ if short
419
+ puts "#{Time.now.strftime("%H:%M:%S")} app:#{application} pkg:#{package} agents:#{total}"
420
+ else
421
+ puts <<-EOF
422
+ at: #{Time.now.inspect}
423
+ package: #{application}/#{package}
424
+ status: #{status['status'].join(',')}
425
+
426
+ participants: #{total} agents
427
+
428
+ EOF
429
+ end
430
+
431
+ %w(fetch prepare switch).each do |key|
432
+ status[key].tap do |st|
433
+ puts "#{key}: queued=#{st['queued'].size}, working=#{st['working'].size}, done=#{st['done'].size}"
434
+ puts " * queued: #{st['queued'].join(', ')}" if !st['queued'].empty? && st['queued'].size != total
435
+ puts " * working: #{st['working'].join(', ')}" if !st['working'].empty? && st['working'].size != total
436
+ puts " * done: #{st['done'].join(', ')}" if !st['done'].empty? && options[:show_done] && st['done'].size != total
437
+ end
438
+ end
439
+
440
+ status
441
+ end
220
442
  end
221
443
 
222
444
  desc "client", "client for master"
@@ -20,6 +20,8 @@ module Mamiya
20
20
  set_default :fetch_sleep, 12
21
21
  set_default :keep_packages, 3
22
22
  set_default :keep_prereleases, 3
23
+ set_default :keep_releases, 3
24
+ set_default :applications, {}
23
25
 
24
26
  # master
25
27
  set_default :master, {monitor: {refresh_interval: nil}} # TODO: don't nest
@@ -27,6 +29,15 @@ module Mamiya
27
29
 
28
30
  add_hook :labels, chain: true
29
31
 
32
+ add_hook :before_deploy_or_rollback
33
+ add_hook :after_deploy_or_rollback
34
+
35
+ add_hook :before_deploy
36
+ add_hook :after_deploy
37
+
38
+ add_hook :before_rollback
39
+ add_hook :after_rollback
40
+
30
41
  def storage_class
31
42
  Storages.find(self[:storage][:type])
32
43
  end
@@ -38,6 +49,13 @@ module Mamiya
38
49
  def prereleases_dir
39
50
  self[:prereleases_dir] && Pathname.new(self[:prereleases_dir])
40
51
  end
52
+
53
+ # XXX: `config.app(app_name).deploy_to` form is better?
54
+ def deploy_to_for(app)
55
+ # XXX: to_sym
56
+ application = applications[app.to_sym] || applications[app.to_s]
57
+ application && application[:deploy_to] && Pathname.new(application[:deploy_to])
58
+ end
41
59
  end
42
60
  end
43
61
 
@@ -4,6 +4,8 @@ require 'thread'
4
4
 
5
5
  require 'mamiya/master'
6
6
  require 'mamiya/master/agent_monitor_handlers'
7
+ require 'mamiya/master/package_status'
8
+ require 'mamiya/master/application_status'
7
9
 
8
10
  module Mamiya
9
11
  class Master
@@ -17,6 +19,8 @@ module Mamiya
17
19
  PACKAGES_QUERY = 'mamiya:packages'.freeze
18
20
  DEFAULT_INTERVAL = 60
19
21
 
22
+ PACKAGE_STATUS_KEYS = %w(packages prereleases releases currents).map(&:freeze).freeze
23
+
20
24
  def initialize(master, raise_exception: false)
21
25
  @master = master
22
26
  @interval = (master.config[:master] &&
@@ -47,6 +51,14 @@ module Mamiya
47
51
  end
48
52
  end
49
53
 
54
+ def package_status(app, pkg, labels: nil)
55
+ PackageStatus.new(self, app, pkg, labels: labels)
56
+ end
57
+
58
+ def application_status(app, labels: nil)
59
+ ApplicationStatus.new(self, app, labels: labels)
60
+ end
61
+
50
62
  def start!
51
63
  @thread ||= Thread.new do
52
64
  loop do
@@ -140,8 +152,9 @@ module Mamiya
140
152
  begin
141
153
  resp = JSON.parse(json)
142
154
 
143
- new_statuses[name]['packages'] = resp['packages']
144
- new_statuses[name]['prereleases'] = resp['prereleases']
155
+ PACKAGE_STATUS_KEYS.each do |k|
156
+ new_statuses[name][k] = resp[k]
157
+ end
145
158
  rescue JSON::ParserError => e
146
159
  logger.warn "Failed to parse packages from #{name}: #{e.message}"
147
160
  next
@@ -149,8 +162,10 @@ module Mamiya
149
162
  end
150
163
 
151
164
  (new_statuses.keys - packages_response["Responses"].keys).each do |name|
152
- if @statuses[name] && @statuses[name]['packages']
153
- new_statuses[name]['packages'] = @statuses[name]['packages']
165
+ PACKAGE_STATUS_KEYS.each do |k|
166
+ if @statuses[name] && @statuses[name][k]
167
+ new_statuses[name][k] = @statuses[name][k]
168
+ end
154
169
  end
155
170
  end
156
171
 
@@ -72,6 +72,10 @@ module Mamiya
72
72
  end
73
73
  end
74
74
 
75
+ def task___switch__finish(status, task)
76
+ status['currents'] ||= {}
77
+ status['currents'][task['app']] = task['pkg']
78
+ end
75
79
 
76
80
 
77
81
  def pkg__remove(status, payload, event)
@@ -85,6 +89,12 @@ module Mamiya
85
89
  prereleases = status['prereleases'][payload['app']]
86
90
  prereleases.delete(payload['pkg']) if prereleases
87
91
  end
92
+
93
+ def release__remove(status, payload, event)
94
+ status['releases'] ||= {}
95
+ releases = status['releases'][payload['app']]
96
+ releases.delete(payload['pkg']) if releases
97
+ end
88
98
  end
89
99
  end
90
100
  end
@@ -0,0 +1,72 @@
1
+ require 'mamiya/master'
2
+
3
+ module Mamiya
4
+ class Master
5
+ ##
6
+ # This class determines application cluster's status (what's majority package active, etc).
7
+ class ApplicationStatus
8
+ def initialize(agent_monitor, application, labels: nil)
9
+ @application = application
10
+ @agents = agent_monitor.statuses(labels: @labels).reject { |_, s| s['master'] }
11
+ @labels = labels
12
+ end
13
+
14
+ attr_reader :labels, :application, :agents
15
+
16
+ def to_hash
17
+ {
18
+ application: application,
19
+ labels: labels,
20
+ participants_count: participants.size,
21
+
22
+ major_current: major_current,
23
+ currents: currents,
24
+
25
+ common_releases: common_releases,
26
+ common_previous_release: common_previous_release,
27
+ }
28
+ end
29
+
30
+ def participants
31
+ @participants ||= Hash[agents.select do |name, status|
32
+ (status['currents'] && status['currents'][application]) || \
33
+ (status['releases'] && status['releases'][application]) || \
34
+ (status['prereleases'] && status['prereleases'][application]) || \
35
+ (status['packages'] && status['packages'][application]) || \
36
+
37
+ status['queues'].any? { |_, q|
38
+ (q['working'] && q['working']['app'] == application) || \
39
+ (q['queue'] && q['queue'].any? { |t| t['app'] == application })
40
+ }
41
+ end]
42
+ end
43
+
44
+ def currents
45
+ @currents ||= Hash[participants.group_by do |name, status|
46
+ status['currents'] && status['currents'][application]
47
+ end.map { |package, as| [package, as.map(&:first).sort] }]
48
+ end
49
+
50
+ def major_current
51
+ @major_current ||= currents.max_by { |package, as| as.size }[0]
52
+ end
53
+
54
+ def common_releases
55
+ #@common_releases ||= participants.map { |_| _[].select { |package, as| as.size > 2 }.map(&:first).compact.sort
56
+ @common_releases ||= participants.
57
+ map { |name, agent| agent['releases'] && agent['releases'][application] }.
58
+ compact.
59
+ inject(:&).
60
+ sort
61
+ end
62
+
63
+ def common_previous_release
64
+ idx = common_releases.index(major_current)
65
+ return if !idx || idx < 1
66
+
67
+ common_releases[idx-1]
68
+ end
69
+ end
70
+ end
71
+ end
72
+