etsy-deployinator 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +368 -0
  5. data/Rakefile +16 -0
  6. data/bin/deployinator-tailer.rb +78 -0
  7. data/deployinator.gemspec +31 -0
  8. data/lib/deployinator.rb +114 -0
  9. data/lib/deployinator/app.rb +204 -0
  10. data/lib/deployinator/base.rb +58 -0
  11. data/lib/deployinator/config.rb +7 -0
  12. data/lib/deployinator/controller.rb +147 -0
  13. data/lib/deployinator/helpers.rb +610 -0
  14. data/lib/deployinator/helpers/deploy.rb +68 -0
  15. data/lib/deployinator/helpers/dsh.rb +42 -0
  16. data/lib/deployinator/helpers/git.rb +348 -0
  17. data/lib/deployinator/helpers/plugin.rb +50 -0
  18. data/lib/deployinator/helpers/stack-tail.rb +32 -0
  19. data/lib/deployinator/helpers/version.rb +62 -0
  20. data/lib/deployinator/helpers/view.rb +67 -0
  21. data/lib/deployinator/logging.rb +16 -0
  22. data/lib/deployinator/plugin.rb +7 -0
  23. data/lib/deployinator/stack-tail.rb +34 -0
  24. data/lib/deployinator/static/css/diff_style.css +283 -0
  25. data/lib/deployinator/static/css/highlight.css +235 -0
  26. data/lib/deployinator/static/css/style.css +1223 -0
  27. data/lib/deployinator/static/js/flot/jquery.flot.min.js +1 -0
  28. data/lib/deployinator/static/js/flot/jquery.flot.selection.js +299 -0
  29. data/lib/deployinator/static/js/jquery-1.8.3.min.js +2 -0
  30. data/lib/deployinator/static/js/jquery-ui-1.8.24.min.js +5 -0
  31. data/lib/deployinator/static/js/jquery.timed_bar.js +36 -0
  32. data/lib/deployinator/tasks/initialize.rake +84 -0
  33. data/lib/deployinator/tasks/tests.rake +22 -0
  34. data/lib/deployinator/templates/deploys_status.mustache +42 -0
  35. data/lib/deployinator/templates/generic_single_push.mustache +64 -0
  36. data/lib/deployinator/templates/index.mustache +12 -0
  37. data/lib/deployinator/templates/layout.mustache +604 -0
  38. data/lib/deployinator/templates/log.mustache +24 -0
  39. data/lib/deployinator/templates/log_table.mustache +90 -0
  40. data/lib/deployinator/templates/messageboxes.mustache +29 -0
  41. data/lib/deployinator/templates/run_logs.mustache +15 -0
  42. data/lib/deployinator/templates/scroll_control.mustache +8 -0
  43. data/lib/deployinator/templates/stream.mustache +13 -0
  44. data/lib/deployinator/version.rb +3 -0
  45. data/lib/deployinator/views/deploys_status.rb +22 -0
  46. data/lib/deployinator/views/index.rb +14 -0
  47. data/lib/deployinator/views/layout.rb +48 -0
  48. data/lib/deployinator/views/log.rb +12 -0
  49. data/lib/deployinator/views/log_table.rb +35 -0
  50. data/lib/deployinator/views/run_logs.rb +32 -0
  51. data/templates/app.rb.erb +7 -0
  52. data/templates/config.ru.erb +10 -0
  53. data/templates/helper.rb.erb +15 -0
  54. data/templates/stack.rb.erb +17 -0
  55. data/templates/template.mustache +1 -0
  56. data/templates/view.rb.erb +7 -0
  57. data/test/unit/helpers_dsh_test.rb +40 -0
  58. data/test/unit/helpers_test.rb +77 -0
  59. data/test/unit/version_test.rb +104 -0
  60. metadata +245 -0
@@ -0,0 +1,68 @@
1
+ module Deployinator
2
+ module Helpers
3
+ # Public: helper methods to interact with deploy processes
4
+ module DeployHelpers
5
+
6
+ # Public: get a list of all currently running deploys
7
+ #
8
+ # Returns an array of hashes of the form {:stack => stackname, :stage
9
+ # => stagename}
10
+ def get_list_of_deploys
11
+ ret = []
12
+ raw = `pgrep -d, -l -f Deployinator`.strip.split(",")
13
+ raw.each do |deploy|
14
+ deploy = deploy.strip
15
+ if deploy =~ /Deployinator - deploy (\S+?):(\S+?)$/
16
+ ret << {:stack => $1, :stage => $2}
17
+ end
18
+ end
19
+ ret
20
+ end
21
+
22
+
23
+ # Public: stop a running deploy indentified by stack and stage
24
+ #
25
+ # Parameters:
26
+ # stack - name of the stack
27
+ # stage - name of the stage
28
+ #
29
+ # Returns true if the deploy was stopped and false on error
30
+ def stop_deploy(stack, stage)
31
+ if deployname = get_deploy_process_title(stack,stage)
32
+ return system("pkill -f '#{deployname}'")
33
+ end
34
+ false
35
+ end
36
+
37
+ # Public: get the activity status of the deploy for a certain stack
38
+ # and stage
39
+ #
40
+ # Parameters:
41
+ # stack - name of the stack
42
+ # stage - name of the stage
43
+ #
44
+ # Returns true for a running deploy or false for a deploy that
45
+ # is not running
46
+ def is_deploy_active?(stack, stage)
47
+ if deployname = get_deploy_process_title(stack,stage)
48
+ return system("pgrep -f '#{deployname}'")
49
+ end
50
+ false
51
+ end
52
+
53
+ # Public: get the process title for the deployment process of a
54
+ # specific stage in a stack
55
+ #
56
+ # Parameters:
57
+ # stack - name of the stack
58
+ # stage - name of the stage
59
+ #
60
+ # Returns the title as a string or nil on error
61
+ def get_deploy_process_title(stack=nil, stage=nil)
62
+ return nil if (stack.nil? or stage.nil?)
63
+ "Deployinator - deploy #{stack}:#{stage}"
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,42 @@
1
+ module Deployinator
2
+ module Helpers
3
+ module DshHelpers
4
+ def dsh_fanout
5
+ @dsh_fanout || 30
6
+ end
7
+
8
+ def run_dsh(groups, cmd, &block)
9
+ groups = [groups] unless groups.is_a?(Array)
10
+ dsh_groups = groups.map {|group| "-g #{group} "}.join("")
11
+ run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh -F #{dsh_fanout} "#{cmd}"}, &block)[:stdout]
12
+ end
13
+
14
+ # run dsh against a given host or array of hosts
15
+ def run_dsh_hosts(hosts, cmd, extra_opts='', &block)
16
+ hosts = [hosts] unless hosts.is_a?(Array)
17
+ if extra_opts.length > 0
18
+ run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} #{extra_opts} -- "#{cmd}"'}, &block
19
+ else
20
+ run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} -- "#{cmd}"'}, &block
21
+ end
22
+ end
23
+
24
+ def run_dsh_extra(dsh_group, cmd, extra_opts, &block)
25
+ # runs dsh to a single group with extra args to dsh
26
+ run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh -g #{dsh_group} -r ssh #{extra_opts} -F #{dsh_fanout} "#{cmd}"}, &block)[:stdout]
27
+ end
28
+
29
+ def hosts_for(group)
30
+ @hosts_for ||= {}
31
+ @hosts_for[group] ||= begin
32
+ dsh_file = "/home/#{Deployinator.default_user}/.dsh/group/#{group}"
33
+ hosts = `ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} cat #{dsh_file}`.chomp
34
+ if $?.nil? || $?.exitstatus != 0
35
+ raise "DSH hosts file at #{Deployinator.deploy_host}:#{dsh_file} is likely missing!"
36
+ end
37
+ hosts.split("\n").delete_if { |x| x.lstrip[0..0] == "#" }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,348 @@
1
+ require 'enumerator'
2
+ require 'date'
3
+
4
+ module Deployinator
5
+ module Helpers
6
+ # Public: module containing helper methods for interacting with git
7
+ # repositories and extracting information from them.
8
+ module GitHelpers
9
+
10
+ # Where we cache the current head rev. stack name wil be appended
11
+ @@rev_head_cache = "/tmp/rev_head_cache"
12
+
13
+ # How many seconds the head rev cache is good for
14
+ @@rev_head_cache_ttl = 15
15
+
16
+ def build_git_cmd(cmd, extra_cmd)
17
+ unless extra_cmd.nil? or extra_cmd.empty?
18
+ "#{extra_cmd} '#{cmd}'"
19
+ else
20
+ cmd
21
+ end
22
+ end
23
+
24
+ # Public: method to get the short rev of a git commit and create a
25
+ # version tag from it. The tag is then dumped into a version text file.
26
+ #
27
+ # stack - String representing the stack, which determines where the
28
+ # version file should be located
29
+ # version_dir - String (or Array of Strings) representing the
30
+ # directories to contain the version file
31
+ # extra_cmd - String representing an additional command to prepend to
32
+ # the version echo command (default: "")
33
+ # path - String containing the base path where the version file is
34
+ # located (default: nil)
35
+ # rev - String containing the rev to parse for the short SHA id
36
+ # (default: "HEAD")
37
+ #
38
+ #
39
+ # Returns STDOUT of the echo command
40
+ def git_bump_version(stack, version_dir, extra_cmd="", path=nil, rev="HEAD", tee_cmd="tee")
41
+ unless version_dir.kind_of?(Array)
42
+ version_dir = [version_dir]
43
+ end
44
+
45
+ ts = Time.now.strftime("%Y%m%d-%H%M%S-%Z")
46
+
47
+ path ||= git_checkout_path(checkout_root, stack)
48
+
49
+ cmd = "cd #{path} && git rev-parse --short=#{Deployinator.git_sha_length} #{rev}"
50
+ cmd = build_git_cmd(cmd, extra_cmd)
51
+ sha1 = run_cmd(cmd)[:stdout]
52
+
53
+ version = "#{sha1.chomp}-#{ts}"
54
+
55
+ fullpaths = ""
56
+ version_dir.each do |dir|
57
+ fullpath = File.join(dir, "version.txt")
58
+ fullpaths << fullpath + " "
59
+ end
60
+
61
+ log_and_stream "Setting #{fullpaths} to #{version}"
62
+
63
+ cmd = "cd #{path} && echo #{version} | #{tee_cmd} #{fullpaths}"
64
+ cmd = build_git_cmd(cmd, extra_cmd)
65
+ run_cmd cmd
66
+
67
+ return version
68
+ end
69
+
70
+ # Public: git helper method to get the path to checkout the git repo to
71
+ #
72
+ # checkout_root - the directory base for the repo location
73
+ # stack - String representing the stack, which determines where the
74
+ # version file should be located
75
+ #
76
+ # Returns path to checkout git repo to
77
+ def git_checkout_path(checkout_root, stack)
78
+ if (git_info_for_stack[stack.intern].has_key?(:checkout_dir))
79
+ dir = git_info_for_stack[stack.intern][:checkout_dir].to_s
80
+ else
81
+ dir = stack.to_s
82
+ end
83
+ File.join(checkout_root, dir)
84
+ end
85
+
86
+ # Public: git helper method to bring a local repo up to date with the
87
+ # remote
88
+ #
89
+ # stack - the stack we want to update the repo for
90
+ # extra_cmd - a command that can be prepended before the actual command
91
+ # path - the path to the repo. This is also passed to the block as
92
+ # an argument if one is provided
93
+ # branch - the branch to checkout after the fetch
94
+ #
95
+ # Returns nothing
96
+ def git_freshen_clone(stack, extra_cmd="", path=nil, branch="master")
97
+ path ||= git_checkout_path(checkout_root, stack)
98
+ cmd = "cd #{path} && git fetch --quiet origin +refs/heads/#{branch}:refs/remotes/origin/#{branch} && git reset --hard origin/#{branch} 2>&1"
99
+ cmd = build_git_cmd(cmd, extra_cmd)
100
+ run_cmd cmd
101
+ yield "#{path}" if block_given?
102
+ end
103
+
104
+ # Public: wrapper function which can be used to clone a non-existing repo
105
+ # or freshen an existing one. It tests if the path exists and fetches
106
+ # remotes if that's the case. Otherwise the repo is checked out at the
107
+ # given path.
108
+ #
109
+ # Examples:
110
+ # git_freshen_or_clone("web", etsy_qa, "/var/etsy/", "master")
111
+ #
112
+ # stack - the stack to refresh or clone the repo for
113
+ # extra_cmd - cmd to prepend before the actual command
114
+ # checkout_root - the directory base for the repo location
115
+ # branch - the branch the repo should be on (currently only used
116
+ # when the repo is refreshed). (default: master)
117
+ # read_write - boolean; True means clone the repo read/write
118
+ #
119
+ # Returns stdout of the respective git command.
120
+ def git_freshen_or_clone(stack, extra_cmd, checkout_root, branch="master", read_write=false)
121
+ path = git_checkout_path(checkout_root, stack)
122
+ is_git = is_git_repo(path, extra_cmd)
123
+ if is_git == :true
124
+ log_and_stream "</br>Refreshing repo #{stack} at #{path}</br>"
125
+ git_freshen_clone(stack, extra_cmd, path, branch)
126
+ elsif is_git == :missing
127
+ log_and_stream "</br>Cloning branch #{branch} of #{stack} repo into #{path}</br>"
128
+ git_clone(stack, git_url(stack, "git", read_write), extra_cmd, checkout_root, branch)
129
+ else
130
+ log_and_stream "</br><span class=\"stderr\">The path for #{stack} at #{path} exists but is not a git repo.</span></br>"
131
+ end
132
+ end
133
+
134
+ # Public: Filters a range of commits and either returns all commit shas that
135
+ # only include the filter_file, or the inverse list of shas.
136
+ #
137
+ # stack - the stack to refresh or clone the repo for
138
+ # extra_cmd - cmd to prepend before the actual command
139
+ # old_rev - the git sha representing an older rev.
140
+ # new_rev - the git sha representing a newer rev.
141
+ # path - optional parameter to overide the path to the git repo.
142
+ # filter_files - filepaths relative to the root of the git repo to use as the filter
143
+ # including - Should the returned list of filters include the filter_file commits, or all other commits
144
+ #
145
+ # Returns an array of shas
146
+ def git_filter_shas(stack, extra_cmd, old_rev, new_rev, path=nil, filter_files=[], including=false)
147
+ path ||= git_checkout_path(checkout_root, stack)
148
+ including_shas = []
149
+ excluding_shas = []
150
+ cmd = "cd #{path} && git log --no-merges --name-only --pretty=format:%H #{old_rev}..#{new_rev}"
151
+ cmd = build_git_cmd(cmd, extra_cmd)
152
+ committers = run_cmd(cmd)[:stdout]
153
+ committers.split(/\n\n/).each { |commit|
154
+ lines = commit.split(/\n/)
155
+ commit_sha = lines.shift
156
+ has_filter_file = false
157
+ filter_files.each { |filter_file|
158
+ if !has_filter_file && lines.include?(filter_file)
159
+ has_filter_file = true
160
+ end
161
+ }
162
+ if has_filter_file
163
+ including_shas.push(commit_sha)
164
+ #add to the exclude shas list as well if there are more than one file
165
+ excluding_shas.push(commit_sha) unless lines.length == 1
166
+ else
167
+ excluding_shas.push(commit_sha)
168
+ end
169
+ }
170
+ if including
171
+ return including_shas
172
+ else
173
+ return excluding_shas
174
+ end
175
+ end
176
+
177
+ # Public: Handles get and store of the head rev from a local
178
+ # file cache. Call this instead of get_git_head_rev.
179
+ #
180
+ # Paramaters:
181
+ # stack - name of the stack for the repo
182
+ # branch - name of the branch to test
183
+ #
184
+ # Returns the rev as a string
185
+ def git_head_rev(stack, branch="master")
186
+ filename = "#{@@rev_head_cache}_#{stack}"
187
+ head_rev = get_from_cache(filename, @@rev_head_cache_ttl)
188
+
189
+ unless head_rev
190
+ head_rev = get_git_head_rev(stack, branch)
191
+ File.open(filename, 'w') {|f| f.write(head_rev) }
192
+ end
193
+
194
+ return head_rev
195
+ end
196
+
197
+ # Public: get the short sha of the remote HEAD rev of a stack repo.
198
+ # Beware that a true git short rev can also be longer than git_sha_length chars and
199
+ # this way of retrieving it is no guarantee to get a unique short rev.
200
+ # But the alternative is cloning the repo and do a git rev-parse --short
201
+ #
202
+ # Parameters:
203
+ # stack - name of the stack for the repo
204
+ # branch - name of the branch to test (default: master)
205
+ #
206
+ # Returns the rev as a string
207
+ def get_git_head_rev(stack, branch='master', protocol='git')
208
+ cmd = %x{git ls-remote -h #{git_url(stack, protocol)} #{branch} | cut -c1-#{Deployinator.git_sha_length}}.chomp
209
+ end
210
+
211
+ # Public: helper method which wraps git clone
212
+ #
213
+ # Examples:
214
+ # git_clone('web', 'git@github.com:web.git)
215
+ #
216
+ # stack - the stack to clone
217
+ # repo_url - the remote url of the repo to clone
218
+ # extra_cmd - command to prepend before the actual command
219
+ # checkout_root - base directory to clone into
220
+ # branch - Git branch to checkout. Defaults to 'master'.
221
+ #
222
+ # Returns nothing
223
+ def git_clone(stack, repo_url, extra_cmd="", checkout_root=checkout_root, branch='master')
224
+ path = git_checkout_path(checkout_root, stack)
225
+ cmd = "git clone #{repo_url} -b #{branch} #{path}"
226
+ cmd = build_git_cmd(cmd, extra_cmd)
227
+ run_cmd cmd
228
+ end
229
+
230
+ # Public: helper method to build github urls
231
+ #
232
+ # Example:
233
+ # git_url('web', 'git', false)
234
+ #
235
+ # stack - the stack whose github url we want
236
+ # protocol - 'https', 'http' or 'git'
237
+ # read_write - if true then we give back a git url we can push to
238
+ #
239
+ # Returns string
240
+ def git_url(stack, protocol="git", read_write=false)
241
+ stack = stack.intern
242
+ repo = git_info_for_stack[stack][:repository]
243
+ github_host = which_github_host(stack)
244
+ repo += ".git" if protocol != "git"
245
+ if (read_write)
246
+ return "git@#{github_host}:#{git_info_for_stack[stack][:user]}/#{repo}"
247
+ else
248
+ return "#{protocol}://#{github_host}/#{git_info_for_stack[stack][:user]}/#{repo}"
249
+ end
250
+ end
251
+
252
+ # Public: helper method to determine which github hostname to use
253
+ #
254
+ # Example:
255
+ # which_github_host('statsd')
256
+ #
257
+ # stack - the stack for the github host we are looking for
258
+ #
259
+ # Returns string
260
+ def which_github_host(stack)
261
+ github_host = git_info_for_stack[stack][:host]
262
+ github_host || Deployinator.github_host
263
+ end
264
+
265
+ def git_info_for_stack
266
+ if Deployinator.git_info_for_stack
267
+ Deployinator.git_info_for_stack
268
+ else
269
+ {}
270
+ end
271
+ end
272
+
273
+ # Public: determines whether a given filesystem path is a git repo
274
+ #
275
+ # path - the path to the directory to test
276
+ # extra_cmd - optional extra_cmd to prepend to the testing command
277
+ # (default: "")
278
+ #
279
+ # Examples
280
+ # is_git_repo('/home/dev/repo')
281
+ # is_git_repo('/home/dev/repo', 'ssh dev@deployhost')
282
+ #
283
+ # Returns :true if it is a git repo,
284
+ # :false if the path exists but is not a git repo,
285
+ # :missing if the path doesn't exist
286
+ def is_git_repo(path, extra_cmd="")
287
+ cmd = "#{extra_cmd} test"
288
+ is_dir = system("#{cmd} -d #{path}")
289
+ is_file = system("#{cmd} -f #{path}")
290
+ is_git = system("#{cmd} -d #{path}/.git")
291
+
292
+ # check possibilities
293
+ if is_dir
294
+ return :true if is_git
295
+ return :false
296
+ elsif is_file
297
+ return :false
298
+ else
299
+ return :missing
300
+ end
301
+ end
302
+
303
+ # Public: determine whether we want to use the github diff based on the
304
+ # existence of the stack key in the github info dict
305
+ #
306
+ # Returns true for existing keys and false otherwise
307
+ def use_github_diff
308
+ git_info_for_stack.has_key? @stack.to_sym
309
+ end
310
+
311
+ def github_list_committers(github_commits_param)
312
+ commits = {}
313
+ unless github_commits_param.nil?
314
+ github_commits_param.each do |c|
315
+ name = c["commit"]["committer"]["name"]
316
+ sha = c["sha"]
317
+ message = c["commit"]["message"]
318
+ commits[sha] = { :name => name, :message => message}
319
+ end
320
+ end
321
+ return commits
322
+ end
323
+
324
+ # Public: list the files that changes between 2 revs
325
+ #
326
+ # Parameters:
327
+ # rev1: string of the older rev
328
+ # rev2: string of the newer rev
329
+ # ssh_cmd: string ssh cmd to get to a host where you've got this repo checked out
330
+ # extra: string any extra cmds like cd that you need to do on the remote host to get to your checkout
331
+ # quiet: boolean - if true we make no additional output and just return the files
332
+ #
333
+ # Returns:
334
+ # Array of files names changed between these revs
335
+ def git_show_changed_files(rev1, rev2, ssh_cmd, extra=nil, quiet=false)
336
+ cmd = %Q{git log --name-only --pretty=oneline --full-index #{rev1}..#{rev2} | grep -vE '^[0-9a-f]{40} ' | sort | uniq}
337
+ extra = "#{extra} &&" unless extra.nil?
338
+ if quiet
339
+ list_of_touched_files = %x{#{ssh_cmd} "#{extra} #{cmd}"}
340
+ else
341
+ list_of_touched_files = run_cmd(%Q{#{ssh_cmd} "#{extra} #{cmd}"})[:stdout]
342
+ end
343
+ return list_of_touched_files.split("\n")
344
+ end
345
+
346
+ end
347
+ end
348
+ end