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,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