mamiya 0.0.1.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +16 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +43 -0
  8. data/Rakefile +6 -0
  9. data/bin/mamiya +17 -0
  10. data/config.example.yml +11 -0
  11. data/docs/sequences/deploy.png +0 -0
  12. data/docs/sequences/deploy.uml +58 -0
  13. data/example.rb +74 -0
  14. data/lib/mamiya.rb +5 -0
  15. data/lib/mamiya/agent.rb +181 -0
  16. data/lib/mamiya/agent/actions.rb +12 -0
  17. data/lib/mamiya/agent/fetcher.rb +137 -0
  18. data/lib/mamiya/agent/handlers/abstract.rb +20 -0
  19. data/lib/mamiya/agent/handlers/fetch.rb +68 -0
  20. data/lib/mamiya/cli.rb +322 -0
  21. data/lib/mamiya/cli/client.rb +172 -0
  22. data/lib/mamiya/config.rb +57 -0
  23. data/lib/mamiya/dsl.rb +192 -0
  24. data/lib/mamiya/helpers/git.rb +75 -0
  25. data/lib/mamiya/logger.rb +190 -0
  26. data/lib/mamiya/master.rb +118 -0
  27. data/lib/mamiya/master/agent_monitor.rb +146 -0
  28. data/lib/mamiya/master/agent_monitor_handlers.rb +44 -0
  29. data/lib/mamiya/master/web.rb +148 -0
  30. data/lib/mamiya/package.rb +122 -0
  31. data/lib/mamiya/script.rb +117 -0
  32. data/lib/mamiya/steps/abstract.rb +19 -0
  33. data/lib/mamiya/steps/build.rb +72 -0
  34. data/lib/mamiya/steps/extract.rb +26 -0
  35. data/lib/mamiya/steps/fetch.rb +24 -0
  36. data/lib/mamiya/steps/push.rb +34 -0
  37. data/lib/mamiya/storages.rb +17 -0
  38. data/lib/mamiya/storages/abstract.rb +48 -0
  39. data/lib/mamiya/storages/mock.rb +61 -0
  40. data/lib/mamiya/storages/s3.rb +127 -0
  41. data/lib/mamiya/util/label_matcher.rb +38 -0
  42. data/lib/mamiya/version.rb +3 -0
  43. data/mamiya.gemspec +35 -0
  44. data/misc/logger_test.rb +12 -0
  45. data/spec/agent/actions_spec.rb +37 -0
  46. data/spec/agent/fetcher_spec.rb +199 -0
  47. data/spec/agent/handlers/fetch_spec.rb +121 -0
  48. data/spec/agent_spec.rb +255 -0
  49. data/spec/config_spec.rb +50 -0
  50. data/spec/dsl_spec.rb +291 -0
  51. data/spec/fixtures/dsl_test_load.rb +1 -0
  52. data/spec/fixtures/dsl_test_use.rb +1 -0
  53. data/spec/fixtures/helpers/foo.rb +1 -0
  54. data/spec/fixtures/test-package-source/.mamiya.meta.json +1 -0
  55. data/spec/fixtures/test-package-source/greeting +1 -0
  56. data/spec/fixtures/test-package.tar.gz +0 -0
  57. data/spec/fixtures/test.yml +4 -0
  58. data/spec/logger_spec.rb +68 -0
  59. data/spec/master/agent_monitor_spec.rb +269 -0
  60. data/spec/master/web_spec.rb +121 -0
  61. data/spec/master_spec.rb +94 -0
  62. data/spec/package_spec.rb +394 -0
  63. data/spec/script_spec.rb +78 -0
  64. data/spec/spec_helper.rb +38 -0
  65. data/spec/steps/build_spec.rb +261 -0
  66. data/spec/steps/extract_spec.rb +68 -0
  67. data/spec/steps/fetch_spec.rb +96 -0
  68. data/spec/steps/push_spec.rb +73 -0
  69. data/spec/storages/abstract_spec.rb +22 -0
  70. data/spec/storages/s3_spec.rb +342 -0
  71. data/spec/storages_spec.rb +33 -0
  72. data/spec/support/dummy_serf.rb +70 -0
  73. data/spec/util/label_matcher_spec.rb +85 -0
  74. metadata +272 -0
@@ -0,0 +1,12 @@
1
+ module Mamiya
2
+ class Agent
3
+ module Actions
4
+ def distribute(application, package)
5
+ trigger('fetch',
6
+ application: application,
7
+ package: package,
8
+ )
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,137 @@
1
+ require 'thread'
2
+ require 'mamiya/steps/fetch'
3
+
4
+ require 'mamiya/storages/abstract'
5
+
6
+ module Mamiya
7
+ class Agent
8
+ ##
9
+ # This class has a queue for fetching packages.
10
+ class Fetcher
11
+ GRACEFUL_TIMEOUT = 60
12
+
13
+ def initialize(config, logger: Mamiya::Logger.new)
14
+ @thread = nil
15
+ @queue = Queue.new
16
+
17
+ @config = config
18
+ @destination = config[:packages_dir]
19
+ @keep_packages = config[:keep_packages]
20
+ @current_job = nil
21
+
22
+ @logger = logger['fetcher']
23
+ @working = nil
24
+ end
25
+
26
+ attr_reader :thread
27
+ attr_reader :current_job
28
+ attr_writer :cleanup_hook
29
+
30
+ def enqueue(app, package, before: nil, &callback)
31
+ @queue << [app, package, before, callback]
32
+ end
33
+
34
+ def queue_size
35
+ @queue.size
36
+ end
37
+
38
+ def start!
39
+ @logger.info 'Starting...'
40
+
41
+ @thread = Thread.new(&method(:main_loop))
42
+ @thread.abort_on_exception = true
43
+ end
44
+
45
+ def stop!(graceful = false)
46
+ return unless @thread
47
+
48
+ if graceful
49
+ @queue << :suicide
50
+ @thread.join(GRACEFUL_TIMEOUT)
51
+ end
52
+
53
+ @thread.kill if @thread.alive?
54
+ ensure
55
+ @thread = nil
56
+ end
57
+
58
+ def running?
59
+ @thread && @thread.alive?
60
+ end
61
+
62
+ def working?
63
+ !!@current_job
64
+ end
65
+
66
+ def cleanup
67
+ Dir[File.join(@destination, '*')].each do |app|
68
+ packages = Dir[File.join(app, "*.tar.gz")]
69
+ packages.sort_by! { |_| [File.mtime(_), _] }
70
+ packages[0...-@keep_packages].each do |victim|
71
+ @logger.info "Cleaning up: remove #{victim}"
72
+ File.unlink(victim) if File.exist?(victim)
73
+
74
+ meta_victim = victim.sub(/\.tar\.gz\z/, '.json')
75
+ if File.exist?(meta_victim)
76
+ @logger.info "Cleaning up: remove #{meta_victim}"
77
+ File.unlink(meta_victim)
78
+ end
79
+
80
+ package_name = File.basename(victim, '.tar.gz')
81
+ if @cleanup_hook
82
+ @cleanup_hook.call(File.basename(app), package_name)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def main_loop
91
+ while order = @queue.pop
92
+ break if order == :suicide
93
+ handle_order(*order)
94
+ end
95
+ end
96
+
97
+ def handle_order(app, package, before_hook = nil, callback = nil)
98
+ @current_job = [app, package]
99
+ @logger.info "fetching #{app}:#{package}"
100
+ # TODO: Limit apps by configuration
101
+
102
+ destination = File.join(@destination, app)
103
+
104
+ Dir.mkdir(destination) unless File.exist?(destination)
105
+
106
+ before_hook.call if before_hook
107
+
108
+ # TODO: before run hook for agent.update_tags!
109
+ Mamiya::Steps::Fetch.new(
110
+ application: app,
111
+ package: package,
112
+ destination: destination,
113
+ config: @config,
114
+ ).run!
115
+
116
+ callback.call if callback
117
+
118
+ @logger.info "fetched #{app}:#{package}"
119
+
120
+ cleanup
121
+
122
+ rescue Mamiya::Storages::Abstract::AlreadyFetched => e
123
+ @logger.info "skipped #{app}:#{package} (already fetched)"
124
+ callback.call(e) if callback
125
+ rescue Exception => e
126
+ @logger.fatal "fetch failed (#{app}:#{package}): #{e.inspect}"
127
+ e.backtrace.each do |line|
128
+ @logger.fatal "\t#{line}"
129
+ end
130
+
131
+ callback.call(e) if callback
132
+ ensure
133
+ @current_job = nil
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+
3
+ module Mamiya
4
+ class Agent
5
+ module Handlers
6
+ class Abstract
7
+ def initialize(agent, event)
8
+ @agent = agent
9
+ @event = event
10
+ @payload = (event.payload && !event.payload.empty?) ? JSON.parse(event.payload) : {}
11
+ end
12
+
13
+ attr_reader :agent, :event, :payload
14
+
15
+ def run!
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,68 @@
1
+ require 'mamiya/agent/handlers/abstract'
2
+ require 'mamiya/storages/abstract'
3
+
4
+ module Mamiya
5
+ class Agent
6
+ module Handlers
7
+ class Fetch < Abstract
8
+ FETCH_ACK_EVENT = 'mamiya:fetch-result:ack'
9
+ FETCH_START_EVENT = 'mamiya:fetch-result:start'
10
+ FETCH_SUCCESS_EVENT = 'mamiya:fetch-result:success'
11
+ FETCH_ERROR_EVENT = 'mamiya:fetch-result:error'
12
+
13
+ IGNORED_ERRORS = [
14
+ Mamiya::Storages::Abstract::AlreadyFetched.new(''),
15
+ ].freeze
16
+
17
+ def run!
18
+ agent.serf.event(FETCH_ACK_EVENT,
19
+ {
20
+ name: agent.serf.name,
21
+ application: payload['application'],
22
+ package: payload['package'],
23
+ pending: agent.fetcher.queue_size.succ,
24
+ }.to_json
25
+ )
26
+
27
+ agent.fetcher.enqueue(
28
+ payload['application'], payload['package'],
29
+ before: proc {
30
+ agent.serf.event(FETCH_START_EVENT,
31
+ {
32
+ name: agent.serf.name,
33
+ application: payload['application'],
34
+ package: payload['package'],
35
+ pending: agent.fetcher.queue_size.succ,
36
+ }.to_json
37
+ )
38
+ agent.update_tags!
39
+ }
40
+ ) do |error|
41
+ if error && IGNORED_ERRORS.lazy.grep(error.class).none?
42
+ agent.serf.event(FETCH_ERROR_EVENT,
43
+ {
44
+ name: agent.serf.name,
45
+ application: payload['application'],
46
+ package: payload['package'],
47
+ error: error.inspect,
48
+ pending: agent.fetcher.queue_size,
49
+ }.to_json
50
+ )
51
+ else
52
+ agent.serf.event(FETCH_SUCCESS_EVENT,
53
+ {
54
+ name: agent.serf.name,
55
+ application: payload['application'],
56
+ package: payload['package'],
57
+ pending: agent.fetcher.queue_size,
58
+ }.to_json
59
+ )
60
+ end
61
+
62
+ agent.update_tags!
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/mamiya/cli.rb ADDED
@@ -0,0 +1,322 @@
1
+ require 'mamiya'
2
+
3
+ require 'mamiya/config'
4
+ require 'mamiya/script'
5
+ require 'mamiya/logger'
6
+
7
+ require 'mamiya/steps/build'
8
+ require 'mamiya/steps/push'
9
+ require 'mamiya/steps/fetch'
10
+ require 'mamiya/steps/extract'
11
+
12
+ require 'mamiya/agent'
13
+ require 'mamiya/master'
14
+
15
+ require 'thor'
16
+ require 'thread'
17
+
18
+ module Mamiya
19
+ class CLI < Thor
20
+ class_option :config, aliases: '-C', type: :string
21
+ class_option :script, aliases: '-S', type: :string
22
+ class_option :application, aliases: %w(-a --app), type: :string
23
+ class_option :debug, aliases: %w(-d), type: :boolean
24
+ class_option :color, type: :boolean
25
+ class_option :no_color, type: :boolean
26
+ # TODO: class_option :set, aliases: '-s', type: :array
27
+
28
+ no_commands do
29
+ def invoke_command(*)
30
+ super
31
+ rescue SystemExit
32
+ rescue Exception => e
33
+ logger.fatal "#{e.class}: #{e.message}"
34
+
35
+ e.backtrace.map{ |_| _.prepend("\t") }.each do |line|
36
+ logger.debug line
37
+ end
38
+ end
39
+ end
40
+
41
+ desc "status", "Show status of servers"
42
+ def status
43
+ # TODO:
44
+ end
45
+
46
+ desc "list-packages", "List packages in storage"
47
+ method_option :name_only, aliases: '-n'
48
+ def list_packages
49
+ unless options[:name_only]
50
+ puts "Available packages in #{application}:"
51
+ puts ""
52
+ end
53
+
54
+ puts storage.packages.sort
55
+ end
56
+
57
+ desc "list-applications", "List applications in storage"
58
+ def list_applications
59
+ puts _applications.keys
60
+ end
61
+
62
+ desc "show PACKAGE", "Show package"
63
+ method_option :format, aliases: %w(-f), type: :string, default: 'pp'
64
+ def show(package)
65
+ meta = storage.meta(package)
66
+
67
+ case options[:format]
68
+ when 'pp'
69
+ require 'pp'
70
+ pp meta
71
+ when 'json'
72
+ require 'json'
73
+ puts meta.to_json
74
+ when 'yaml'
75
+ require 'yaml'
76
+ puts meta.to_yaml
77
+ end
78
+ end
79
+
80
+ # ---
81
+
82
+ desc "deploy PACKAGE", "Run build->push->distribute->prepare->finalize"
83
+ def deploy
84
+ end
85
+
86
+ desc "rollback", "Switch back to previous release then finalize"
87
+ def rollback
88
+ end
89
+
90
+ desc "build", "Build package."
91
+ method_option :build_from, aliases: %w(--source -f), type: :string
92
+ method_option :build_to, aliases: %w(--destination -t), type: :string
93
+ method_option :skip_prepare_build, aliases: %w(--no-prepare-build -P), type: :boolean
94
+ method_option :force_prepare_build, aliases: %w(--prepare-build -p), type: :boolean
95
+ def build
96
+ # TODO: overriding name
97
+ %i(build_from build_to).each { |k| script.set(k, File.expand_path(options[k])) if options[k] }
98
+
99
+ if options[:force_prepare_build] && options[:skip_prepare_build]
100
+ logger.warn 'Both force_prepare_build and skip_prepare_build are enabled. ' \
101
+ 'This results skipping prepare_build...'
102
+ end
103
+
104
+ if options[:force_prepare_build]
105
+ script.set :skip_prepare_build, false
106
+ end
107
+
108
+ if options[:skip_prepare_build]
109
+ script.set :skip_prepare_build, true
110
+ end
111
+
112
+ Mamiya::Steps::Build.new(script: script, logger: logger).run!
113
+ end
114
+
115
+ desc "push PACKAGE", "Upload built packages to storage."
116
+ def push(package_atom)
117
+ package_path = package_path_from_atom(package_atom)
118
+
119
+ if options[:application]
120
+ logger.warn "Overriding package's application name with given one: #{options[:application]}"
121
+ sleep 2
122
+ end
123
+
124
+ Mamiya::Steps::Push.new(
125
+ config: config,
126
+ package: package_path,
127
+ application: options[:application],
128
+ ).run!
129
+ end
130
+
131
+ desc "fetch PACKAGE DESTINATION", "Retrieve package from storage"
132
+ def fetch(package_atom, destination)
133
+ Mamiya::Steps::Fetch.new(
134
+ script: script(:no_error),
135
+ config: config,
136
+ package: package_atom,
137
+ application: application,
138
+ destination: destination,
139
+ ).run!
140
+ end
141
+
142
+ desc "extract PACKAGE DESTINATION", "Unpack package to DESTINATION"
143
+ def extract(package_atom, destination)
144
+ package_path = package_path_from_atom(package_atom)
145
+
146
+ Mamiya::Steps::Extract.new(
147
+ package: package_path,
148
+ destination: destination
149
+ ).run!
150
+ end
151
+
152
+ desc "distribute PACKAGE", "Order clients to download specified package."
153
+ def distribute
154
+ end
155
+
156
+ desc "prepare", "Prepare package on clients."
157
+ def prepare
158
+ end
159
+
160
+ desc "finalize", "Finalize (start) prepared package on clients."
161
+ def finalize
162
+ end
163
+
164
+
165
+ # ---
166
+
167
+ desc "agent", "Start agent."
168
+ method_option :serf, type: :array
169
+ method_option :daemonize, aliases: '-D', type: :boolean, default: false
170
+ method_option :log, aliases: '-l', type: :string
171
+ method_option :pidfile, aliases: '-p', type: :string
172
+ def agent
173
+ prepare_agent_behavior!
174
+ merge_serf_option!
175
+
176
+ agent = Agent.new(config, logger: logger)
177
+ agent.run!
178
+ end
179
+
180
+ desc "master", "Start master"
181
+ method_option :serf, type: :array
182
+ method_option :daemonize, aliases: '-D', type: :boolean, default: false
183
+ method_option :log, aliases: '-l', type: :string
184
+ method_option :pidfile, aliases: '-p', type: :string
185
+ def master
186
+ prepare_agent_behavior!
187
+ merge_serf_option!
188
+
189
+ agent = Master.new(config, logger: logger)
190
+ agent.run!
191
+ end
192
+
193
+ # def worker
194
+ # end
195
+
196
+ # def event_handler
197
+ # end
198
+
199
+ private
200
+
201
+ def prepare_agent_behavior!
202
+ pidfile = File.expand_path(options[:pidfile]) if options[:pidfile]
203
+ logger # insitantiate
204
+
205
+ Process.daemon(:nochdir) if options[:daemonize]
206
+
207
+ if pidfile
208
+ open(pidfile, 'w') { |io| io.puts $$ }
209
+ at_exit { File.unlink(pidfile) if File.exist?(pidfile) }
210
+ end
211
+
212
+ reload_queue = Queue.new
213
+ reload_thread = Thread.new do
214
+ while reload_queue.pop
215
+ logger.info "Reopening"
216
+ logger.reopen
217
+ logger.info "Log reopened"
218
+ end
219
+ end
220
+ reload_thread.abort_on_exception = true
221
+
222
+ trap(:HUP) do
223
+ reload_queue << true
224
+ end
225
+ end
226
+
227
+ def config(dont_raise_error = false)
228
+ return @config if @config
229
+ path = [options[:config], './mamiya.yml', './config.yml'].compact.find { |_| File.exists?(_) }
230
+
231
+ if path
232
+ logger.debug "Using configuration: #{path}"
233
+ @config = Mamiya::Config.load(File.expand_path(path))
234
+ else
235
+ logger.debug "Couldn't find configuration file"
236
+ return nil if dont_raise_error
237
+ fatal! "Configuration File not found (try --config(-C) option or place it at ./mamiya.yml or ./config.yml)"
238
+ end
239
+ end
240
+
241
+ def script(dont_raise_error = false)
242
+ return @script if @script
243
+ path = [options[:script], './mamiya.rb', './deploy.rb'].compact.find { |_| File.exists?(_) }
244
+
245
+ if path
246
+ logger.debug "Using deploy script: #{path}"
247
+ @script = Mamiya::Script.new.load!(File.expand_path(path)).tap do |s|
248
+ s.set :application, options[:application] if options[:application]
249
+ s.set :logger, logger
250
+ end
251
+ else
252
+ logger.debug "Couldn't find deploy script."
253
+ return nil if dont_raise_error
254
+ fatal! "Deploy Script File not found (try --script(-S) option or place it at ./mamiya.rb or ./deploy.rb)"
255
+ end
256
+ end
257
+
258
+ def fatal!(message)
259
+ logger.fatal message
260
+ exit 1
261
+ end
262
+
263
+ def merge_serf_option!
264
+ (config[:serf] ||= {})[:agent] ||= {}
265
+
266
+ options[:serf].flat_map{ |_| _.split(/,/) }.each do |conf|
267
+ k,v = conf.split(/=/,2)
268
+ config[:serf][:agent][k.to_sym] = v
269
+ end
270
+ end
271
+
272
+ def application
273
+ options[:application] || config[:application] || script.application
274
+ end
275
+
276
+ def storage
277
+ config.storage_class.new(
278
+ config[:storage].merge(
279
+ application: application
280
+ )
281
+ )
282
+ end
283
+
284
+ def _applications
285
+ config.storage_class.find(config[:storage])
286
+ end
287
+
288
+ def logger
289
+ @logger ||= begin
290
+ $stdout.sync = ENV["MAMIYA_SYNC_OUT"] == '1'
291
+ outs = [$stdout]
292
+ outs << File.expand_path(options[:log]) if options[:log]
293
+ Mamiya::Logger.new(
294
+ color: options[:no_color] ? false : (options[:color] ? true : nil),
295
+ outputs: outs,
296
+ level: options[:debug] ? Mamiya::Logger::DEBUG : Mamiya::Logger.defaults[:level],
297
+ )
298
+ end
299
+ end
300
+
301
+ def package_path_from_atom(package_atom)
302
+ candidates = [
303
+ package_atom,
304
+ options[:build_to] && File.join(options[:build_to], package_atom),
305
+ options[:build_to] && File.join(options[:build_to], "#{package_atom}.tar.gz"),
306
+ script(:no_error) && script.build_to && File.join(script.build_to, package_atom),
307
+ script(:no_error) && script.build_to && File.join(script.build_to, "#{package_atom}.tar.gz"),
308
+ ]
309
+ logger.debug "Candidates: #{candidates.inspect}"
310
+
311
+ package_path = candidates.select { |_| _ }.find { |_| File.exists?(_) }
312
+
313
+ unless package_path
314
+ fatal! "Package (#{package_atom}) couldn't find at #{candidates.join(', ')}"
315
+ end
316
+
317
+ package_path
318
+ end
319
+ end
320
+ end
321
+
322
+ require 'mamiya/cli/client'