mamiya 0.0.1.alpha2

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 (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'