etsy-deployinator 1.0.2 → 1.1.0
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.
- 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
|
|