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,172 @@
1
+ require 'mamiya/cli'
2
+
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'uri'
6
+ require 'json'
7
+ require 'thor'
8
+
9
+ module Mamiya
10
+ class CLI < Thor
11
+ class Client < Thor
12
+ class_option :master, aliases: '-u', type: :string
13
+ class_option :application, aliases: %w(-a --app), type: :string
14
+
15
+ desc "list-applications", "list applications"
16
+ def list_applications
17
+ puts master_get('/packages')["applications"]
18
+ end
19
+
20
+ desc "list-packages", "list-packages"
21
+ def list_packages
22
+ puts master_get("/packages/#{application}")["packages"]
23
+ end
24
+
25
+ desc "show-package", "show package meta data"
26
+ method_option :format, aliases: %w(-f), type: :string, default: 'pp'
27
+ def show_package(package)
28
+ meta = master_get("/packages/#{application}/#{package}")
29
+
30
+ case options[:format]
31
+ when 'pp'
32
+ require 'pp'
33
+ pp meta
34
+ when 'json'
35
+ require 'json'
36
+ puts meta.to_json
37
+ when 'yaml'
38
+ require 'yaml'
39
+ puts meta.to_yaml
40
+ end
41
+ end
42
+
43
+ desc "list-agents", 'list agents'
44
+ def list_agents
45
+ payload = master_get("/agents")
46
+
47
+ agents = payload["agents"].keys
48
+
49
+ agents.each do |agent|
50
+ puts "#{agent}\talive"
51
+ end
52
+ payload["failed_agents"].each do |agent|
53
+ puts "#{agent}\tfailed"
54
+ end
55
+ end
56
+
57
+ desc "show-agent AGENT", 'Show agent'
58
+ method_option :format, aliases: %w(-f), type: :string, default: 'pp'
59
+ def show_agent(agent)
60
+ agent = master_get("/agents/#{agent}")
61
+
62
+ case options[:format]
63
+ when 'pp'
64
+ require 'pp'
65
+ pp agent
66
+ when 'json'
67
+ require 'json'
68
+ puts agent.to_json
69
+ when 'yaml'
70
+ require 'yaml'
71
+ puts agent.to_yaml
72
+ end
73
+ end
74
+
75
+ desc "show-distribution package", "Show package distribution status"
76
+ method_option :format, aliases: %w(-f), type: :string, default: 'text'
77
+ method_option :verbose, aliases: %w(-v), type: :boolean
78
+ def show_distribution(package)
79
+ dist = master_get("/packages/#{application}/#{package}/distribution")
80
+
81
+ case options[:format]
82
+ when 'json'
83
+ require 'json'
84
+ puts dist.to_json
85
+ return
86
+
87
+ when 'yaml'
88
+ require 'yaml'
89
+ puts dist.to_yaml
90
+ return
91
+ end
92
+
93
+ total = dist['distributed_count'] + dist['not_distributed_count']
94
+ puts <<-EOF
95
+ Distribution status of #{application}/#{package}
96
+
97
+ ratio (distributed:total-agents): #{"%.1f" % ((dist['distributed_count']/total.to_f)*100)}%
98
+ number of agents have package: #{dist['distributed_count']}
99
+ number of agents don't have package: #{dist['not_distributed_count']}
100
+ EOF
101
+
102
+ if options[:verbose]
103
+ puts ""
104
+ dist['not_distributed'].each do |name|
105
+ puts "#{name}\tnot_distributed"
106
+ end
107
+ dist['distributed'].each do |name|
108
+ puts "#{name}\tdistributed"
109
+ end
110
+ end
111
+ end
112
+
113
+ desc "distribute package", "order distributing package to agents"
114
+ def distribute(package)
115
+ p master_post("/packages/#{application}/#{package}/distribute")
116
+ end
117
+
118
+ desc "refresh", "order refreshing agent status"
119
+ def refresh
120
+ p master_post('/agents/refresh')
121
+ end
122
+
123
+ desc "deploy PACKAGE", "Run distribute->prepare->finalize"
124
+ def deploy
125
+ end
126
+
127
+ desc "rollback", "Switch back to previous release then finalize"
128
+ def rollback
129
+ end
130
+
131
+ private
132
+
133
+ def fatal!(msg)
134
+ $stderr.puts msg
135
+ exit 1
136
+ end
137
+
138
+ def application
139
+ options[:application] or fatal!('specify application')
140
+ end
141
+
142
+ def master_get(path)
143
+ master_http.start do |http|
144
+ JSON.parse http.get(path).tap(&:value).body
145
+ end
146
+ end
147
+
148
+ def master_post(path, data='')
149
+ master_http.start do |http|
150
+ response = http.post(path, data).tap(&:value)
151
+ response.code == '204' ? true : JSON.parse(response.tap(&:value).body)
152
+ end
153
+ end
154
+
155
+ def master_http
156
+ url = master_url
157
+ Net::HTTP.new(url.host, url.port).tap do |http|
158
+ http.use_ssl = true if url.scheme == 'https'
159
+ end
160
+ end
161
+
162
+ def master_url
163
+ url = ENV["MAMIYA_MASTER_URL"] || options[:master]
164
+ fatal! 'specify master URL via --master(-u) option or $MAMIYA_MASTER_URL' unless url
165
+ URI.parse(url)
166
+ end
167
+ end
168
+
169
+ desc "client", "client for master"
170
+ subcommand "client", Client
171
+ end
172
+ end
@@ -0,0 +1,57 @@
1
+ require 'yaml'
2
+ require 'mamiya/storages'
3
+
4
+ module Mamiya
5
+ class Config
6
+ def self.load(file)
7
+ self.new YAML.load_file(file)
8
+ end
9
+
10
+ def initialize(config_hash = {})
11
+ @config = symbolize_keys_in(config_hash)
12
+ end
13
+
14
+ def [](key)
15
+ @config[key]
16
+ end
17
+
18
+ def []=(key, value)
19
+ @config[key] = value
20
+ end
21
+
22
+ def storage_class
23
+ self[:storage] && Storages.find(self[:storage][:type])
24
+ end
25
+
26
+ def deploy_to_for_app(app)
27
+ # TODO: test
28
+ app = app.to_sym
29
+
30
+ if self[:apps] && self[:applications][app]
31
+ Pathname.new(self[:applications][app][:deploy_to])
32
+ end
33
+ end
34
+
35
+ def releases_path_for_app(app)
36
+ # TODO: test
37
+ deploy_to_for_app(app).join('releases')
38
+ end
39
+
40
+ private
41
+
42
+ def symbolize_keys_in(hash)
43
+ Hash[hash.map { |k, v|
44
+ case v
45
+ when Hash
46
+ v = symbolize_keys_in(v)
47
+ when Array
48
+ if v.find { |_| _.kind_of?(Hash) }
49
+ v = v.map { |_| _.kind_of?(Hash) ? symbolize_keys_in(_) : _ }
50
+ end
51
+ end
52
+
53
+ [k.to_sym, v]
54
+ }]
55
+ end
56
+ end
57
+ end
data/lib/mamiya/dsl.rb ADDED
@@ -0,0 +1,192 @@
1
+ require 'mamiya/util/label_matcher'
2
+ require 'thread'
3
+
4
+ module Mamiya
5
+ class DSL
6
+ class TaskNotDefinedError < Exception; end
7
+ class HelperNotFound < Exception; end
8
+
9
+ ##
10
+ # Creates new DSL environment.
11
+ def initialize
12
+ @variables = {}
13
+ @tasks = {}
14
+ @hooks = {}
15
+ @eval_lock = Mutex.new
16
+ @use_lock = Mutex.new
17
+ end
18
+
19
+ attr_reader :hooks, :tasks
20
+
21
+ ##
22
+ # Returns Hash of default setting variables.
23
+ def self.defaults
24
+ @defaults ||= {}
25
+ end
26
+
27
+ def self.define_variable_accessor(name) # :nodoc:
28
+ k = name.to_sym
29
+ return if self.instance_methods.include?(k)
30
+
31
+ define_method(k) { @variables[k] || self.class.defaults[k] }
32
+ end
33
+
34
+ ##
35
+ # Sets default value +value+ for variable name +key+.
36
+ # Values set by this method will available for all instances of same class.
37
+ def self.set_default(key, value)
38
+ k = key.to_sym
39
+ defaults[k] = value
40
+ self.define_variable_accessor(k)
41
+ end
42
+
43
+ ##
44
+ # Add hook point with name +name+.
45
+ # This defines method with same name in class to call and define hooks.
46
+ def self.add_hook(name, attributes={})
47
+ define_method(name) do |*args, &block|
48
+ @hooks[name] ||= []
49
+
50
+ if block
51
+ hook_name = args.shift if args.first.kind_of?(String)
52
+ options = args.pop if args.last.kind_of?(Hash)
53
+
54
+ hook = {block: block, options: options || {}, name: hook_name}
55
+ case args.first
56
+ when :overwrite
57
+ @hooks[name] = [hook]
58
+ when :prepend
59
+ @hooks[name][0,0] = [hook]
60
+ else
61
+ @hooks[name] << hook
62
+ end
63
+
64
+ else
65
+ matcher = Mamiya::Util::LabelMatcher::Simple.new(args)
66
+ Proc.new { |*args|
67
+ filtered_hooks = @hooks[name].reject { |hook|
68
+ options = hook[:options]
69
+
70
+ (options[:only] && !matcher.match?(*options[:only] )) ||
71
+ (options[:except] && matcher.match?(*options[:except]))
72
+ }
73
+
74
+ if attributes[:chain]
75
+ init = args.shift
76
+ filtered_hooks.inject(init) do |result, hook|
77
+ hook[:block].call(result, *args)
78
+ end
79
+ else
80
+ filtered_hooks.each do |hook|
81
+ hook[:block].call *args
82
+ end
83
+ end
84
+ }
85
+ end
86
+ end
87
+ end
88
+
89
+ ##
90
+ # :call-seq:
91
+ # evaluate!(string [, filename [, lineno]])
92
+ # evaluate! { block }
93
+ #
94
+ # Evaluates given string or block in DSL environment.
95
+ def evaluate!(str = nil, filename = nil, lineno = nil, &block)
96
+ @eval_lock.synchronize {
97
+ begin
98
+ if block_given?
99
+ self.instance_eval(&block)
100
+ elsif str
101
+ @file = filename if filename
102
+
103
+ if str && filename && lineno
104
+ self.instance_eval(str, filename, lineno)
105
+ elsif str && filename
106
+ self.instance_eval(str, filename)
107
+ elsif str
108
+ self.instance_eval(str)
109
+ end
110
+ end
111
+ ensure
112
+ @file = nil
113
+ end
114
+ }
115
+ self
116
+ end
117
+
118
+ ##
119
+ # Evaluates specified file +file+ in DSL environment.
120
+ def load!(file)
121
+ evaluate! File.read(file), file, 1
122
+ end
123
+
124
+ ##
125
+ # (DSL) Find file using +name+ from current +load_path+ then load.
126
+ # +options+ will be available as variable +options+ in loaded file.
127
+ def use(name, options={})
128
+ helper_file = find_helper_file(name)
129
+ raise HelperNotFound unless helper_file
130
+
131
+ @use_lock.lock unless @use_lock.owned? # to avoid lock recursively
132
+
133
+ @_options = options
134
+ self.instance_eval File.read(helper_file).prepend("options = @_options; @_options = nil;\n"), helper_file, 1
135
+
136
+ ensure
137
+ @_options = nil
138
+ @use_lock.unlock if @use_lock.owned?
139
+ end
140
+
141
+ ##
142
+ # (DSL) Set value +value+ for variable named +key+.
143
+ def set(key, value)
144
+ k = key.to_sym
145
+ self.class.define_variable_accessor(key) unless self.methods.include?(k)
146
+ @variables[k] = value
147
+ end
148
+
149
+ ##
150
+ # (DSL) Set value +value+ for variable named +key+ unless value is present for the variable.
151
+ def set_default(key, value)
152
+ k = key.to_sym
153
+ return @variables[k] if @variables.key?(k)
154
+ set(k, value)
155
+ end
156
+
157
+ ##
158
+ # (DSL) Define task named +name+ with given block.
159
+ def task(name, &block)
160
+ @tasks[name] = block
161
+ end
162
+
163
+ ##
164
+ # (DSL) Invoke task named +name+.
165
+ def invoke(name)
166
+ raise TaskNotDefinedError unless @tasks[name]
167
+ self.instance_eval &@tasks[name]
168
+ end
169
+
170
+ ##
171
+ # Returns current load path used by +use+ method.
172
+ def load_path
173
+ (@variables[:load_path] ||= []) +
174
+ [
175
+ "#{__dir__}/helpers",
176
+ *(@file ? ["#{File.dirname(@file)}/helpers"] : [])
177
+ ]
178
+ end
179
+
180
+ private
181
+
182
+ def find_helper_file(name) # :nodoc:
183
+ load_path.find do |_| # Using find to return nil when not found
184
+ path = File.join(_, "#{name}.rb")
185
+ break path if File.exists?(path)
186
+ end
187
+ end
188
+
189
+ # TODO: hook call context methods
190
+ #https://gist.github.com/sorah/9263951
191
+ end
192
+ end
@@ -0,0 +1,75 @@
1
+ require 'time'
2
+
3
+ set_default :git_remote, 'origin'
4
+ set_default :commit, "#{self.git_remote}/HEAD"
5
+
6
+ def git_ignored_files
7
+ git_clean_out = `git clean -ndx`.lines
8
+ prefix = /^Would (?:remove|skip repository) /
9
+
10
+ if git_clean_out.any? { |_| prefix !~ _ }
11
+ puts git_clean_out
12
+ raise "`git clean -ndx` doesn't return line starting with 'Would remove' or 'Would skip'"
13
+ end
14
+
15
+ excludes = git_clean_out.grep(prefix).map{ |_| _.sub(prefix, '').chomp }
16
+ if package_under
17
+ excludes.grep(/^#{Regexp.escape(package_under)}/).map{ |_| _.sub(/^#{Regexp.escape(package_under)}\/?/, '') }
18
+ else
19
+ excludes
20
+ end
21
+ end
22
+
23
+ def git_head
24
+ git_show = `git show --pretty=fuller -s`
25
+ commit, comment = git_show.split(/\n\n/, 2)
26
+
27
+ {
28
+ commit: commit.match(/^commit (.+)$/)[1],
29
+ author: commit.match(/^Author:\s*(?<name>.+?) <(?<email>.+?)>$/).
30
+ tap {|match| break Hash[match.names.map {|name| [name.to_sym, match[name]] }] },
31
+ author_date: Time.parse(commit.match(/^AuthorDate:\s*(.+)$/)[1]),
32
+ committer: commit.match(/^Commit:\s*(?<name>.+?) <(?<email>.+?)>$/).
33
+ tap {|match| break Hash[match.names.map {|name| [name.to_sym, match[name]] }] },
34
+ commit_date: Time.parse(commit.match(/^CommitDate:\s*(.+)$/)[1]),
35
+ }
36
+ end
37
+
38
+ prepare_build do |update|
39
+ logger = self.logger['git']
40
+
41
+ if !update && !self.repository
42
+ logger.warn 'Skipping cloning repository because script.repository not set'
43
+ elsif !update
44
+ run "git", "clone", self.repository, self.build_from
45
+ end
46
+
47
+ Dir.chdir(self.build_from) do
48
+ logger.info Dir.pwd
49
+ run "git", "fetch", self.git_remote
50
+ run "git", "remote", "prune", self.git_remote, allow_failure: true
51
+ run "git", "fetch", "--tags", self.git_remote
52
+
53
+ run "git", "reset", "--hard", self.commit
54
+ end
55
+ end
56
+
57
+ if options[:exclude_git_clean_targets]
58
+ build(:prepend) do
59
+ set :exclude_from_package, exclude_from_package + git_ignored_files()
60
+ end
61
+ end
62
+
63
+ options[:add_commit_hash_to_package_name] = true unless options.key?(:add_commit_hash_to_package_name)
64
+ if options[:add_commit_hash_to_package_name]
65
+ package_name do |candidate|
66
+ candidate + [git_head[:commit]]
67
+ end
68
+ end
69
+
70
+ options[:include_head_commit_to_meta] = true unless options.key?(:include_head_commit_to_meta)
71
+ if options[:include_head_commit_to_meta]
72
+ package_meta do |candidate|
73
+ candidate.merge(git: git_head())
74
+ end
75
+ end