etsy-deployinator 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +0 -1
- data/README.md +47 -10
- data/Rakefile +0 -1
- data/bin/deployinator-tailer.rb +2 -2
- data/deployinator.gemspec +5 -1
- data/lib/deployinator.rb +20 -1
- data/lib/deployinator/app.rb +53 -12
- data/lib/deployinator/controller.rb +8 -3
- data/lib/deployinator/helpers.rb +109 -13
- data/lib/deployinator/helpers/concurrency.rb +70 -0
- data/lib/deployinator/helpers/dsh.rb +15 -5
- data/lib/deployinator/helpers/git.rb +16 -9
- data/lib/deployinator/helpers/version.rb +52 -52
- data/lib/deployinator/static/css/maintenance.css +9 -0
- data/lib/deployinator/static/css/style.css +25 -5
- data/lib/deployinator/static/images/maintenance.gif +0 -0
- data/lib/deployinator/static/js/stats_load.js +10 -0
- data/lib/deployinator/templates/exception.mustache +11 -0
- data/lib/deployinator/templates/generic_single_push.mustache +2 -1
- data/lib/deployinator/templates/layout.mustache +5 -4
- data/lib/deployinator/templates/log_table.mustache +18 -5
- data/lib/deployinator/templates/maintenance.mustache +5 -0
- data/lib/deployinator/templates/stats.mustache +180 -0
- data/lib/deployinator/version.rb +1 -1
- data/lib/deployinator/views/log_table.rb +36 -0
- data/lib/deployinator/views/maintenance.rb +15 -0
- data/lib/deployinator/views/stats.rb +96 -0
- data/test/unit/concurrency_test.rb +72 -0
- data/test/unit/git_test.rb +70 -0
- data/test/unit/helpers_test.rb +61 -2
- data/test/unit/version_test.rb +12 -12
- metadata +74 -17
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'celluloid/autostart'
|
3
|
+
|
4
|
+
module Deployinator
|
5
|
+
module Helpers
|
6
|
+
module ConcurrencyHelpers
|
7
|
+
extend Celluloid
|
8
|
+
# Hash of future objects that have been instantiated so far
|
9
|
+
@@futures = {}
|
10
|
+
# Public: run block of code in parallel
|
11
|
+
#
|
12
|
+
# Returns Handle to future object created
|
13
|
+
def run_parallel(name, &block)
|
14
|
+
name = name.to_sym
|
15
|
+
if reference_taken? name
|
16
|
+
raise DuplicateReferenceError, "Name #{name} already taken for future."
|
17
|
+
end
|
18
|
+
log_and_stream '</br>Queueing execution of future: ' + name.to_s + '</br>'
|
19
|
+
@@futures[name] = Celluloid::Future.new do
|
20
|
+
# Set filename for thread
|
21
|
+
runlog_filename(name)
|
22
|
+
# setting up separate logger
|
23
|
+
log_and_stream '</br>Starting execution of future: ' + name.to_s + '</br>'
|
24
|
+
block.call
|
25
|
+
end
|
26
|
+
@@futures[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
# Public: check if the reference name for future is taken
|
33
|
+
#
|
34
|
+
# Returns boolean
|
35
|
+
def reference_taken?(name)
|
36
|
+
return @@futures.has_key?(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: returns the value of the code block execution
|
40
|
+
# This also sends all the logged data in stream back to main
|
41
|
+
# log file and removes the temporary log file created for the thread
|
42
|
+
# Returns the return value of the last line executed in block
|
43
|
+
def get_value(future, timeout=nil)
|
44
|
+
if @filename
|
45
|
+
file_path = "#{RUN_LOG_PATH}" + runlog_thread_filename(future)
|
46
|
+
return_value = @@futures[future.to_sym].value(timeout)
|
47
|
+
log_and_stream File.read(file_path)
|
48
|
+
File.delete(file_path) if File.exists?(file_path)
|
49
|
+
return_value
|
50
|
+
else
|
51
|
+
@@futures[future.to_sym].value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: returns the values of the specified futures
|
56
|
+
#
|
57
|
+
# Returns hash of values
|
58
|
+
def get_values(*futures)
|
59
|
+
value_hash = {}
|
60
|
+
futures.each do |future|
|
61
|
+
value_hash[future] = get_value(future)
|
62
|
+
end
|
63
|
+
value_hash
|
64
|
+
end
|
65
|
+
|
66
|
+
class DuplicateReferenceError < StandardError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -5,14 +5,19 @@ module Deployinator
|
|
5
5
|
@dsh_fanout || 30
|
6
6
|
end
|
7
7
|
|
8
|
-
def run_dsh(groups, cmd, &block)
|
8
|
+
def run_dsh(groups, cmd, only_stdout=true, &block)
|
9
9
|
groups = [groups] unless groups.is_a?(Array)
|
10
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)
|
11
|
+
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh -F #{dsh_fanout} "#{cmd}"}, &block)
|
12
|
+
if only_stdout
|
13
|
+
cmd_return[:stdout]
|
14
|
+
else
|
15
|
+
cmd_return
|
16
|
+
end
|
12
17
|
end
|
13
18
|
|
14
19
|
# run dsh against a given host or array of hosts
|
15
|
-
def run_dsh_hosts(hosts, cmd, extra_opts='', &block)
|
20
|
+
def run_dsh_hosts(hosts, cmd, extra_opts='', only_stdout=true, &block)
|
16
21
|
hosts = [hosts] unless hosts.is_a?(Array)
|
17
22
|
if extra_opts.length > 0
|
18
23
|
run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} #{extra_opts} -- "#{cmd}"'}, &block
|
@@ -21,9 +26,14 @@ module Deployinator
|
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
24
|
-
def run_dsh_extra(dsh_group, cmd, extra_opts, &block)
|
29
|
+
def run_dsh_extra(dsh_group, cmd, extra_opts, only_stdout=true, &block)
|
25
30
|
# 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)
|
31
|
+
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh -g #{dsh_group} -r ssh #{extra_opts} -F #{dsh_fanout} "#{cmd}"}, &block)
|
32
|
+
if only_stdout
|
33
|
+
cmd_return[:stdout]
|
34
|
+
else
|
35
|
+
cmd_return
|
36
|
+
end
|
27
37
|
end
|
28
38
|
|
29
39
|
def hosts_for(group)
|
@@ -93,10 +93,16 @@ module Deployinator
|
|
93
93
|
# branch - the branch to checkout after the fetch
|
94
94
|
#
|
95
95
|
# Returns nothing
|
96
|
-
def git_freshen_clone(stack, extra_cmd="", path=nil, branch="master")
|
96
|
+
def git_freshen_clone(stack, extra_cmd="", path=nil, branch="master", force_checkout=false)
|
97
97
|
path ||= git_checkout_path(checkout_root, stack)
|
98
|
-
cmd =
|
99
|
-
|
98
|
+
cmd = [
|
99
|
+
"cd #{path}",
|
100
|
+
"git fetch --quiet origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}",
|
101
|
+
"git reset --hard origin/#{branch} 2>&1",
|
102
|
+
"git checkout #{'--force' if force_checkout} #{branch} 2>&1",
|
103
|
+
]
|
104
|
+
cmd << "git reset --hard origin/#{branch} 2>&1"
|
105
|
+
cmd = build_git_cmd(cmd.join(" && "), extra_cmd)
|
100
106
|
run_cmd cmd
|
101
107
|
yield "#{path}" if block_given?
|
102
108
|
end
|
@@ -117,15 +123,15 @@ module Deployinator
|
|
117
123
|
# read_write - boolean; True means clone the repo read/write
|
118
124
|
#
|
119
125
|
# Returns stdout of the respective git command.
|
120
|
-
def git_freshen_or_clone(stack, extra_cmd, checkout_root, branch="master", read_write=false)
|
126
|
+
def git_freshen_or_clone(stack, extra_cmd, checkout_root, branch="master", read_write=false, protocol="git", force_checkout=false)
|
121
127
|
path = git_checkout_path(checkout_root, stack)
|
122
128
|
is_git = is_git_repo(path, extra_cmd)
|
123
129
|
if is_git == :true
|
124
130
|
log_and_stream "</br>Refreshing repo #{stack} at #{path}</br>"
|
125
|
-
git_freshen_clone(stack, extra_cmd, path, branch)
|
131
|
+
git_freshen_clone(stack, extra_cmd, path, branch, force_checkout)
|
126
132
|
elsif is_git == :missing
|
127
133
|
log_and_stream "</br>Cloning branch #{branch} of #{stack} repo into #{path}</br>"
|
128
|
-
git_clone(stack, git_url(stack,
|
134
|
+
git_clone(stack, git_url(stack, protocol, read_write), extra_cmd, checkout_root, branch)
|
129
135
|
else
|
130
136
|
log_and_stream "</br><span class=\"stderr\">The path for #{stack} at #{path} exists but is not a git repo.</span></br>"
|
131
137
|
end
|
@@ -188,7 +194,7 @@ module Deployinator
|
|
188
194
|
|
189
195
|
unless head_rev
|
190
196
|
head_rev = get_git_head_rev(stack, branch)
|
191
|
-
|
197
|
+
write_to_cache(filename, head_rev)
|
192
198
|
end
|
193
199
|
|
194
200
|
return head_rev
|
@@ -329,11 +335,12 @@ module Deployinator
|
|
329
335
|
# ssh_cmd: string ssh cmd to get to a host where you've got this repo checked out
|
330
336
|
# extra: string any extra cmds like cd that you need to do on the remote host to get to your checkout
|
331
337
|
# quiet: boolean - if true we make no additional output and just return the files
|
338
|
+
# diff_filter: string to pass to git to make it only show certain types of changes (added/removed)
|
332
339
|
#
|
333
340
|
# Returns:
|
334
341
|
# 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}
|
342
|
+
def git_show_changed_files(rev1, rev2, ssh_cmd, extra=nil, quiet=false, diff_filter="")
|
343
|
+
cmd = %Q{git log --name-only --pretty=oneline --full-index #{rev1}..#{rev2} --diff-filter=#{diff_filter} | grep -vE '^[0-9a-f]{40} ' | sort | uniq}
|
337
344
|
extra = "#{extra} &&" unless extra.nil?
|
338
345
|
if quiet
|
339
346
|
list_of_touched_files = %x{#{ssh_cmd} "#{extra} #{cmd}"}
|
@@ -1,62 +1,62 @@
|
|
1
1
|
module Deployinator
|
2
2
|
module Helpers
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
3
|
+
module VersionHelpers
|
4
|
+
# Public: wrapper function to get the short SHA of a revision. The function
|
5
|
+
# checks retrieves the part of the string before the first dash. If the part
|
6
|
+
# is a valid default git short rev, i.e. alphanumeric and length 7 it is
|
7
|
+
# returned. For an invalid rev, nil is returned.
|
8
|
+
#
|
9
|
+
# ver - String representing the revision
|
10
|
+
#
|
11
|
+
# Returns the short SHA consisting of the alphanumerics until the first dash
|
12
|
+
# or nil for an invalid version string
|
13
|
+
def get_build(ver)
|
14
|
+
# return the short sha of the rev
|
15
|
+
the_sha = (ver || "")[/^([^-]+)/]
|
16
|
+
# check that we have a default git SHA
|
17
|
+
val = /^[a-zA-Z0-9]{7,}$/.match the_sha
|
18
|
+
val.nil? ? nil : the_sha
|
19
|
+
end
|
20
20
|
module_function :get_build
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
# Public: function to get the current software version running on a host
|
23
|
+
#
|
24
|
+
# host - String of the hostname to check
|
25
|
+
#
|
26
|
+
# Returns the full version of the current software running on the host
|
27
|
+
def get_version(host)
|
28
|
+
host_url = "https://#{host}/"
|
29
|
+
get_version_by_url("#{host_url}version.txt")
|
30
|
+
end
|
31
31
|
module_function :get_version
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
# Public: function to fetch a version string from a URL. The version string
|
34
|
+
# is validated to have a valid format. The function calls a lower level
|
35
|
+
# implementation method for actually getting the version.
|
36
|
+
#
|
37
|
+
# url - String representing where to get the version from
|
38
|
+
#
|
39
|
+
# Returns the version string or nil if the format is invalid
|
40
|
+
def get_version_by_url(url)
|
41
|
+
version = curl_get_url(url)
|
42
|
+
val = /^[a-zA-Z0-9]{7,}-[0-9]{8}-[0-9]{6}-UTC$/.match version
|
43
|
+
val.nil? ? nil : version.chomp
|
44
|
+
end
|
45
45
|
module_function :get_version_by_url
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
47
|
+
# Public: this helper function wraps the actual call to get the contents of a
|
48
|
+
# version file. This helps with reducing code duplication and also stubbing
|
49
|
+
# out the actual call for unit testing.
|
50
|
+
#
|
51
|
+
# url - String representing the complete URL to query
|
52
|
+
#
|
53
|
+
# Returns the contents of the URL resource
|
54
|
+
def curl_get_url(url)
|
55
|
+
with_timeout 2, "getting version via curl from #{url}" do
|
56
|
+
`curl -s #{url}`
|
57
|
+
end
|
58
|
+
end
|
59
59
|
module_function :curl_get_url
|
60
|
-
|
61
|
-
|
60
|
+
end
|
61
|
+
end
|
62
62
|
end
|
@@ -941,19 +941,29 @@ section#main.config{
|
|
941
941
|
/*consol*/
|
942
942
|
.code {
|
943
943
|
background-color: #222;
|
944
|
-
color: #
|
945
|
-
font-
|
944
|
+
color: #ddd;
|
945
|
+
font-size: 16px;
|
946
946
|
padding: 8px;
|
947
|
+
|
947
948
|
}
|
948
949
|
|
949
950
|
.code .command {
|
951
|
+
font-family: monospace;
|
952
|
+
font-size:14px;
|
953
|
+
margin-top:2px;
|
950
954
|
border-top: 1px solid #555;
|
951
|
-
padding: 20px
|
955
|
+
padding: 10px 20px 5px 10px;
|
952
956
|
}
|
953
957
|
.code .command h4 {
|
954
|
-
color: #
|
958
|
+
color: #ccc;
|
955
959
|
font-weight: normal;
|
956
960
|
}
|
961
|
+
|
962
|
+
.code .command h5 {
|
963
|
+
color: #abc;
|
964
|
+
font-size: 12px;
|
965
|
+
margin-bottom:10px;
|
966
|
+
}
|
957
967
|
.code .command p {
|
958
968
|
margin: 10px;
|
959
969
|
}
|
@@ -967,6 +977,17 @@ section#main.config{
|
|
967
977
|
font-size: 12px;
|
968
978
|
white-space: pre;
|
969
979
|
}
|
980
|
+
.code .success_msg {
|
981
|
+
color: #7fbf4d;
|
982
|
+
font-size: 12px;
|
983
|
+
white-space: pre;
|
984
|
+
}
|
985
|
+
.code .warning_msg {
|
986
|
+
color: #f0ad4e;
|
987
|
+
font-size: 12px;
|
988
|
+
white-space: pre;
|
989
|
+
}
|
990
|
+
|
970
991
|
.hidden {
|
971
992
|
display:none;
|
972
993
|
}
|
@@ -1135,7 +1156,6 @@ header.web_config {
|
|
1135
1156
|
margin-top: 20px;
|
1136
1157
|
-webkit-column-width: 220px;
|
1137
1158
|
-moz-column-width: 220px;
|
1138
|
-
height: 420px;
|
1139
1159
|
}
|
1140
1160
|
|
1141
1161
|
.pinned-stack-list {
|
Binary file
|
@@ -0,0 +1,10 @@
|
|
1
|
+
$(function() {
|
2
|
+
$.each(data, function(key, val) {
|
3
|
+
$("#choices").append('<h3><input type="checkbox" name="' + key +
|
4
|
+
'" checked="checked" id="id' + key + '">' +
|
5
|
+
'<label for="id' + key + '">'+ val.label + '</label></h3>');
|
6
|
+
});
|
7
|
+
|
8
|
+
$("#choices").find("input").click(drawGraphs);
|
9
|
+
drawGraphs();
|
10
|
+
})
|
@@ -50,8 +50,9 @@
|
|
50
50
|
<div class="push-topic">
|
51
51
|
<div class="title"></div>
|
52
52
|
<span class="log-run">
|
53
|
-
<form action="/
|
53
|
+
<form action="/log">
|
54
54
|
<button class="button small" type="submit">See run logs</button>
|
55
|
+
<input type="hidden" name="stack" value="{{stack}}" />
|
55
56
|
</form>
|
56
57
|
</span>
|
57
58
|
</div>
|
@@ -2,10 +2,10 @@
|
|
2
2
|
<html>
|
3
3
|
<head>
|
4
4
|
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
5
|
-
<title>Deployinator</title>
|
5
|
+
<title>Deployinator{{#stack}} - {{stack}}{{/stack}}</title>
|
6
6
|
<link rel="shortcut icon" href="/favicon.ico">
|
7
7
|
<link rel="stylesheet" href="/static/css/highlight.css" type="text/css" media="screen">
|
8
|
-
<link rel="stylesheet" href="/static/css/style.css?v=
|
8
|
+
<link rel="stylesheet" href="/static/css/style.css?v=11" type="text/css" media="screen">
|
9
9
|
<link rel="stylesheet" href="/static/css/diff_style.css" type="text/css" media="screen">
|
10
10
|
<script src="/js/jquery-1.8.3.min.js"></script>
|
11
11
|
<script src="/js/jquery-ui-1.8.24.min.js"></script>
|
@@ -53,6 +53,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
53
53
|
<span class="label">Stack: </span>
|
54
54
|
<span>
|
55
55
|
<select name='stacks' id='stacks'>
|
56
|
+
<option>Choose...</option>
|
56
57
|
{{#get_stack_select}}
|
57
58
|
<option value="{{stack}}"{{#current}} selected{{/current}}>{{stack}}</option>
|
58
59
|
{{/get_stack_select}}
|
@@ -99,7 +100,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
99
100
|
window.location = '/' + stack;
|
100
101
|
});
|
101
102
|
|
102
|
-
// listen for changes on the watch button
|
103
|
+
// listen for changes on the watch button
|
103
104
|
$('.watch').click(function () {
|
104
105
|
$.ajax({
|
105
106
|
url : '/{{stack}}/can-deploy',
|
@@ -110,7 +111,7 @@ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __
|
|
110
111
|
console.log(form);
|
111
112
|
if (form.length == 1) {
|
112
113
|
stream_log_websocket(form[0]);
|
113
|
-
}
|
114
|
+
}
|
114
115
|
}
|
115
116
|
});
|
116
117
|
});
|
@@ -3,7 +3,12 @@
|
|
3
3
|
<tr>
|
4
4
|
<th>
|
5
5
|
{{# prev_page }}
|
6
|
-
|
6
|
+
<form action="/log">
|
7
|
+
{{#prev_page_params}}
|
8
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
9
|
+
{{/prev_page_params}}
|
10
|
+
<button class="button small">Prev</button>
|
11
|
+
</form>
|
7
12
|
{{/ prev_page }}
|
8
13
|
</th>
|
9
14
|
<th></th>
|
@@ -11,9 +16,13 @@
|
|
11
16
|
<th></th>
|
12
17
|
<th></th>
|
13
18
|
<th></th>
|
14
|
-
<th></th>
|
15
19
|
<th>
|
16
|
-
<
|
20
|
+
<form action="/log">
|
21
|
+
{{#next_page_params}}
|
22
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
23
|
+
{{/next_page_params}}
|
24
|
+
<button class="button small">Next</button>
|
25
|
+
</form>
|
17
26
|
</th>
|
18
27
|
</tr>
|
19
28
|
<tr>
|
@@ -67,7 +76,9 @@
|
|
67
76
|
<th>
|
68
77
|
{{# prev_page }}
|
69
78
|
<form action="/log">
|
70
|
-
|
79
|
+
{{#prev_page_params}}
|
80
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
81
|
+
{{/prev_page_params}}
|
71
82
|
<button class="button small">Prev</button>
|
72
83
|
</form>
|
73
84
|
{{/ prev_page }}
|
@@ -79,7 +90,9 @@
|
|
79
90
|
<th></th>
|
80
91
|
<th>
|
81
92
|
<form action="/log">
|
82
|
-
|
93
|
+
{{#next_page_params}}
|
94
|
+
<input type="hidden" name="{{name}}" value="{{value}}" />
|
95
|
+
{{/next_page_params}}
|
83
96
|
<button class="button small">Next</button>
|
84
97
|
</form>
|
85
98
|
|