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,42 @@
1
+ <!-- Depoly Status Page <3 -->
2
+
3
+ <script>
4
+
5
+ function load_deploys(){
6
+ $.get( "/deploys", function( data ) {
7
+ data = JSON.parse(data)
8
+ var items = [];
9
+ var html = ''
10
+ if (data.length===0){
11
+ data.push({"stack":"alcohol","stage":"consuming"})
12
+ }
13
+ for (var i = 0; i < data.length; ++i){
14
+ var stack = data[i]['stack']
15
+ var stage = data[i]['stage']
16
+ html = html +
17
+ "<div class='stack-box'> \
18
+ <div class=\"deploys-listing-heading\"><h2>" + stack + ":" + stage + "</h2></div> \
19
+ <a href=\"javascript:stream_log_websocket_deploys('" + stack + "', '" + stage + "')\">Watch</a> \
20
+ </div>"
21
+ }
22
+ $( "#running-deploys" ).html(html);
23
+ });
24
+ }
25
+
26
+ setInterval( load_deploys, 2000 );
27
+
28
+ </script>
29
+
30
+ <aside id="running-deploys" class="pushers">
31
+ {{#current_deploys}}
32
+ <div class="stack-box">
33
+ <div class="deploys-listing-heading"><h2>{{stack}}:{{stage}}</h2></div>
34
+ <a href="javascript:stream_log_websocket_deploys('{{stack}}', '{{stage}}')">Watch</a>
35
+ </div>
36
+ {{/current_deploys}}
37
+ </aside>
38
+ <section id="main" class="info">
39
+ <div id = "shell_deploys"class="log-main">
40
+ </div>
41
+ </section>
42
+
@@ -0,0 +1,64 @@
1
+ <!-- Stacks info -->
2
+ <section id="info-bar" class="clearfix">
3
+ <ul>
4
+ <li class="heading">Version: </li>
5
+ {{#environments}}
6
+ <li>{{name}} &ndash; <span class="{{stack}}_{{name}}_version">{{current_version}}</span> </li>
7
+ {{/environments}}
8
+ </ul>
9
+ </section>
10
+
11
+ {{> messageboxes }}
12
+
13
+ <aside class="pushers">
14
+
15
+ {{#environments}}
16
+ <div class="stack-box">
17
+ <h2><span>{{number}}</span>{{title}}</h2>
18
+ <form action="/deploys" method="post" data-method="{{stack}}_{{method}}">
19
+ <input type="hidden" name="method" value="{{method}}">
20
+ <input type="hidden" name="stack" value="{{stack}}">
21
+ <input type="hidden" name="env" value="{{name}}">
22
+
23
+ <p class="stack-status">
24
+ <a href="/diff/{{stack}}/{{current_build}}/{{next_build}}{{#use_github_diff}}/github{{/use_github_diff}}" target="_blank">
25
+ <span class="{{stack}}_{{name}}_current_build">{{current_build}}</span>
26
+ &rarr;
27
+ <span class="{{stack}}_{{name}}_next_build">{{next_build}}</span>
28
+ </a>
29
+ </p>
30
+ <p class="stack-push"><button class="button {{#disabled}} button-disabled" disabled="disabled{{/disabled}}" type="submit">Deploy {{name}}</button></p>
31
+ </form>
32
+ {{#not_last}}
33
+ <span class="arrow"></span>
34
+ {{/not_last}}
35
+ </div>
36
+ {{/environments}}
37
+
38
+ </aside>
39
+
40
+ <section id="main" class="info">
41
+ <!-- heading -->
42
+ <div class="heading clearfix">
43
+ <h2>Info</h2>
44
+ <span class="option">
45
+ {{< scroll_control }}
46
+ </span>
47
+ </div>
48
+
49
+ <!-- topic -->
50
+ <div class="push-topic">
51
+ <div class="title"></div>
52
+ <span class="log-run">
53
+ <form action="/run_logs">
54
+ <button class="button small" type="submit">See run logs</button>
55
+ </form>
56
+ </span>
57
+ </div>
58
+
59
+ <!-- log -->
60
+ <div class="log-main">
61
+ {{< log }}
62
+ </div>
63
+
64
+ </section>
@@ -0,0 +1,12 @@
1
+ <!-- Stack Index Page <3 -->
2
+
3
+ <section id="info-bar" class="clearfix">
4
+ <ul><li class="heading">Stack Index</li></ul>
5
+ </section>
6
+ <div class="stack-list stack-box">
7
+ <ul>
8
+ {{#get_other_stack_list}}
9
+ <li><a href="/{{.}}">{{.}}</a></li>
10
+ {{/get_other_stack_list}}
11
+ </ul>
12
+ </div>
@@ -0,0 +1,604 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>Deployinator</title>
6
+ <link rel="shortcut icon" href="/favicon.ico">
7
+ <link rel="stylesheet" href="/static/css/highlight.css" type="text/css" media="screen">
8
+ <link rel="stylesheet" href="/static/css/style.css?v=10" type="text/css" media="screen">
9
+ <link rel="stylesheet" href="/static/css/diff_style.css" type="text/css" media="screen">
10
+ <script src="/js/jquery-1.8.3.min.js"></script>
11
+ <script src="/js/jquery-ui-1.8.24.min.js"></script>
12
+ {{{ additional_header_html }}}
13
+ </head>
14
+ <body>
15
+ {{{ additional_top_body_html }}}
16
+
17
+ {{#dev_context?}}
18
+ <div id="dev">Deployinator is in Dev Context. Pushes will NOT go live.</div>
19
+ {{/dev_context?}}
20
+ <div class="hidden" id="app_context" data-context="{{ app_context }}"></div>
21
+
22
+ <div id="content" class="clearfix">
23
+
24
+ <!-- header -->
25
+ <header class="{{get_header_css_class}} {{stack}} search-{{search_live_cluster}}">
26
+ <div id="dna">
27
+ <a href="/">_________ ______ _____ _____
28
+ ______ /_____ ________ ___ /______ _____ _____(_)_______ ______ ___ /_______ ________
29
+ _ __ / _ _ \___ __ \__ / _ __ \__ / / /__ / __ __ \_ __ `/_ __/_ __ \__ ___/
30
+ / /_/ / / __/__ /_/ /_ / / /_/ /_ /_/ / _ / _ / / // /_/ / / /_ / /_/ /_ /
31
+ \__,_/ \___/ _ .___/ /_/ \____/ _\__, / /_/ /_/ /_/ \__,_/ \__/ \____/ /_/
32
+ /_/ /____/</a>
33
+
34
+ </div>
35
+ <div id="user">
36
+ <div class="line">
37
+ <span class="label">Account: </span>
38
+ <span>
39
+ {{ username }}
40
+ <a href="{{ logout_url }}">logout</a>
41
+ </span>
42
+ </div>
43
+ {{#deploy_host?}}
44
+ <div class="line">
45
+ <span class="label">Deploy Host: </span>
46
+ <span>{{{get_deploy_target_status}}}</span>
47
+ </div>
48
+ {{/deploy_host?}}
49
+ <div class="line">
50
+ {{#can_remove_stack_lock?}}
51
+ <button id="unlockstack">&nbsp;Clear deploy lock&nbsp;</button>
52
+ {{/can_remove_stack_lock?}}
53
+ <span class="label">Stack: </span>
54
+ <span>
55
+ <select name='stacks' id='stacks'>
56
+ {{#get_stack_select}}
57
+ <option value="{{stack}}"{{#current}} selected{{/current}}>{{stack}}</option>
58
+ {{/get_stack_select}}
59
+ </select>
60
+ </span>
61
+ </div>
62
+ <div class="line">
63
+ <span class="label">Current Deploys: </span>
64
+ <span><a title="Wm5sb3JMYmhGdWJoeXFPYmJncG56Y1ZhRnJwaGV2Z2w=" href= "/deploys_status">Watch</a></span>
65
+ </div>
66
+ </div>
67
+ </header>
68
+
69
+ <!-- main content -->
70
+ {{{ yield }}}
71
+
72
+ <!-- footer -->
73
+ <footer>
74
+ <div class="etsy">Etsy</div>
75
+ <div class="source"><a href="http://github.com/etsy/deployinator/tree/master">Deployinator on Github</a></div>
76
+ </footer>
77
+
78
+ <script src="/js/flot/jquery.flot.min.js"></script>
79
+ <script src="/js/flot/jquery.flot.selection.js"></script>
80
+ <script src="/js/jquery.timed_bar.js"></script>
81
+ <script>
82
+ var autoScroll = true;
83
+ var deploy_enabled = true;
84
+ var is_deploying = false;
85
+ var clearing_deploy_lock = false;
86
+ var current_method; // is used by the error report to make the message more specific
87
+ var deploy_buttons;
88
+ var app_context;
89
+ var deployer = "{{ username }}";
90
+ var ircnick; // the irc nick of the logged in user; will be set later on
91
+ var queue_head = ''; // If this stack has a queue then we will be storing a string
92
+ // here to represent the current head of the queue. This will
93
+ // be set in push_topic.mustache and comes from pushbot.
94
+ seen_errors = 0; // GLOBAL
95
+
96
+ // makes the stacks dropdown work
97
+ $('#stacks').change(function () {
98
+ var stack = $(this).val();
99
+ window.location = '/' + stack;
100
+ });
101
+
102
+ // listen for changes on the watch button
103
+ $('.watch').click(function () {
104
+ $.ajax({
105
+ url : '/{{stack}}/can-deploy',
106
+ dataType: 'json',
107
+ timeout: 1001,
108
+ success: function (data) {
109
+ var form = $("form[data-method='" + data.method + "']");
110
+ console.log(form);
111
+ if (form.length == 1) {
112
+ stream_log_websocket(form[0]);
113
+ }
114
+ }
115
+ });
116
+ });
117
+
118
+ // return a more readable method name for showing to the operator
119
+ var human_readable_method_name = function (method_name) {
120
+ if (method_name === "rsync_to_webs") {
121
+ method_name = "production";
122
+ } else if (method_name === "princess_rsync") {
123
+ method_name = "princess";
124
+ }
125
+ return method_name;
126
+ }
127
+
128
+ function close_stream_log_websocket_deploys(id){
129
+ $( "#shell-frame-" + id ).remove();
130
+ }
131
+
132
+ function connect_web_socket(stack, rnd_id, bar, button) {
133
+ var bufferedMsg = '';
134
+ var append;
135
+ var ws = new WebSocket('ws://' + window.location.hostname + ':{{stack_tail_websocket_port}}');
136
+ var show = function(el) {
137
+ return function(msg) {
138
+ bufferedMsg = bufferedMsg + msg;
139
+ if (append) {
140
+ clearTimeout(append);
141
+ }
142
+ append = setTimeout(function() {
143
+ var dom = $(bufferedMsg);
144
+ dom.filter('script#deploy-done').each(function(){
145
+ ws.onclose = function () { };
146
+ ws.close();
147
+ if (bar) {
148
+ bar.replaceWith(button);
149
+ updateVersionsAndBuilds();
150
+ }
151
+ return false;
152
+ });
153
+ el.html(bufferedMsg);
154
+ }, 50);
155
+ }
156
+ }
157
+ ($('#' + rnd_id).contents().find('body.code'));
158
+ ws.onopen = function() { ws.send(stack) };
159
+ ws.onmessage = function(m) {
160
+ if (m.data == '{{stack_tail_version}}') {
161
+ ws.onmessage = function(m) { show(m.data); }
162
+ ws.onclose = function() { show('<div class="command"><h4>Websocket died. Please refresh page to continue viewing log</h4></div>'); }
163
+ } else {
164
+ show('<div class="command"><h4>Version mismatch between front end and back end for tailer. Please refresh page for latest js to fix</h4></div>');
165
+ ws.close();
166
+ }
167
+ };
168
+ return ws;
169
+ }
170
+
171
+ function massage_iframe (rnd_id) {
172
+ $('#' + rnd_id).contents().find('html')[0].innerHTML = '\
173
+ <head>\
174
+ <link rel="stylesheet" href="/static/css/style.css?v=10" type="text/css" media="screen">\
175
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" />\
176
+ <style type="text/css" media="screen">\
177
+ body { background:#111 none repeat scroll 0 0; }\
178
+ </style>\
179
+ </head>\
180
+ <body>\
181
+ </body>';
182
+ $('#' + rnd_id).contents().find('body').addClass('code');
183
+ $('#' + rnd_id).contents().find('body').html('\
184
+ <div class="command">\
185
+ <h4>\
186
+ {{ tailer_loading_message }}\
187
+ </h4>\
188
+ </div>');
189
+ }
190
+
191
+ function create_iframe (rnd_id) {
192
+ var frame = $("<iframe src='about:blank'>").attr("name", rnd_id);
193
+ frame.attr('id', rnd_id);
194
+ frame.addClass("runlogpane");
195
+ // Pauses scrolling if you hover over the iFrame.
196
+
197
+ frame.hover( function() {
198
+ autoScroll = false; },
199
+ function() { autoScroll = true;
200
+ });
201
+ return frame;
202
+ }
203
+
204
+ function stream_log_websocket_deploys (stack, stage) {
205
+
206
+ var rnd_id = "r_" + (Math.random() + "").replace('.', '');
207
+ var name = "<div id=shell-head><div class=deploys-listing-heading>Stack: " + stack + " Stage: " + stage + "</div><a href=\"javascript:close_stream_log_websocket_deploys('" + rnd_id + "')\">Remove</a></div>";
208
+ var whole_shell_frame = $("<div id=shell-frame-" + rnd_id + "></div>");
209
+
210
+ whole_shell_frame.append(name);
211
+ var frame = create_iframe (rnd_id);
212
+ whole_shell_frame.append(frame)
213
+ $("#shell_deploys").append(whole_shell_frame);
214
+ massage_iframe (rnd_id)
215
+ connect_web_socket(stack, rnd_id, null, null);
216
+ }
217
+
218
+
219
+ var stream_log_websocket = function (form) {
220
+ is_deploying = true;
221
+ seen_errors = 0;
222
+ disableDeploys();
223
+
224
+ var button = $(form).find('button');
225
+ var method = $(form).find('input[name="method"]').val();
226
+ var rnd_id = "r_" + (Math.random() + "").replace('.', '');
227
+ var frame = create_iframe (rnd_id);
228
+
229
+ var bar = $('<div>').addClass('push-progress');
230
+
231
+ var $panel = getDeployPanel(method);
232
+
233
+ if ($panel.length > 0) {
234
+ $panel.removeClass("hide");
235
+ $(".progress-panel", $panel).show();
236
+ $(".complete-panel", $panel).hide();
237
+ $panel.find('.run-log-console').html(frame);
238
+ } else {
239
+ $("#main").prepend(frame);
240
+ }
241
+
242
+ var doc = frames[rnd_id].document;
243
+ if (doc) {
244
+ doc.open();
245
+ doc.close();
246
+ }
247
+ massage_iframe (rnd_id)
248
+ // stash the method name in a global we can use later on
249
+ current_method = human_readable_method_name(method);
250
+
251
+ button.replaceWith(bar);
252
+ var extra_class = $(form).attr("class");
253
+ var extra;
254
+ if (extra_class === undefined) {
255
+ extra = "";
256
+ } else {
257
+ extra = extra_class.toLowerCase();
258
+ }
259
+ if (!extra) {
260
+ extra = ".*"; // this gets ti to at least return
261
+ }
262
+
263
+ $.get("/ti/{{stack}}/"+ extra , function(data) {
264
+ bar.timed_bar(data);
265
+ });
266
+
267
+ connect_web_socket('{{stack}}', rnd_id, bar, button);
268
+ }
269
+
270
+ $('body').on("click", '.pushers form[action="/deploys"] button', function() {
271
+ // Check that the user doing the deploy is at the head of the queue
272
+ if (!checkDeployerIsAtHeadOfQueue()) {
273
+ return false;
274
+ }
275
+
276
+ is_deploying = true;
277
+ seen_errors = 0;
278
+ disableDeploys();
279
+
280
+ var button = $(this);
281
+ var form = $(this).closest('form')[0];
282
+ var error_deploying = false;
283
+ var method = $(form).find('input[name="method"]').val();
284
+
285
+ // Manual config editor magic
286
+ if ($(form).data('method') == 'web_config_diff') {
287
+ /* copy the text of the textarea to our form for reviewing the diff */
288
+ $('#production_field').val($("textarea#production").val());
289
+ $("#config-editor").hide();
290
+ }
291
+
292
+ $.ajax({
293
+ type: 'POST',
294
+ url: form.action + '?stack={{stack}}&stage=' + method,
295
+ timeout: 5000,
296
+ data: $(form).serialize(),
297
+ async: false, // makes the parent wait for the response
298
+ success: function (data) {
299
+ },
300
+ error: function (data) {
301
+ error_deploying = true;
302
+ }
303
+ });
304
+
305
+ if (error_deploying) {
306
+ alert('There was an error deploying. Please try again in a moment or there is already a deploy going');
307
+ is_deploying = false;
308
+ enableDeploys();
309
+ return false;
310
+ }
311
+
312
+ stream_log_websocket(form);
313
+
314
+ return false;
315
+ });
316
+
317
+ /*
318
+ * checkDeployerIsAtHeadOfQueue
319
+ * uses global variables: deployer, queue_head and ircnick
320
+ * Determine if the user who pushed the button is at the
321
+ * head of the push queue. If not then put up an alert
322
+ * giving them a chance to not start the deploy.
323
+ */
324
+ var checkDeployerIsAtHeadOfQueue = function () {
325
+ // Only do this check if not in dev context
326
+ if (app_context != 'dev') {
327
+ // Only do this check if there is a push queue on the page
328
+ var waiters = $(".waiters");
329
+ if (waiters.length > 0) {
330
+ $.ajax({
331
+ url: "/auth2nic/" + deployer,
332
+ timeout: 5000,
333
+ async: false, // makes the parent wait for the response
334
+ success: function (data) {
335
+ ircnick = data['irc_nick'];
336
+ },
337
+ error: function (data) {
338
+ ircnick = deployer;
339
+ }
340
+ });
341
+ // Double check we have a nick or fall back to using the auth name
342
+ // both ircnick and deployer are global
343
+ if (typeof ircnick === 'undefined') {
344
+ ircnick = deployer;
345
+ }
346
+
347
+ var in_queue = (queue_head.indexOf(ircnick) >= 0);
348
+ if (!in_queue) {
349
+ if (!confirm("Hey " + deployer + "!\n\nYou do not seem to be in the push queue at the moment. Are you sure you want to deploy?\n\nClick 'OK' to start the deploy or 'Cancel' to not start the deploy.")) {
350
+ return false;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ return true;
356
+ };
357
+
358
+ function getDeployPanel(method) {
359
+ return $("#{{stack}}-"+method+"-panel");
360
+ }
361
+
362
+ function updateVersionsAndBuilds() {
363
+ $.each(["version", "build"], function(i, type) {
364
+ // This finds only the spans at the top of the page that match for version and build if they exist
365
+ if ($("[class^='{{stack}}_'][class$='" + type + "']").not("[class$='current_" + type + "']").not("[class$='next_" + type + "']").length) {
366
+ $.getJSON('/{{stack}}/' + type + 's', function(data) {
367
+ $.each(data, function(index, tuple) {
368
+ $(".{{stack}}_"+tuple[0]+"_"+type).html(function(i, o) {
369
+ if (tuple[1] != o) {
370
+ $(this).effect("highlight", {}, 3000);
371
+ }
372
+ return tuple[1];
373
+ });
374
+ });
375
+ });
376
+ }
377
+ });
378
+ }
379
+
380
+ // Updates the appropriate button text when someone is deploying
381
+ // with the lock status info (who and when)
382
+ function addStackLockStatus(method, who, when) {
383
+ // Find the appropriate stack form
384
+ var stack_form = $('.stack-box form').has('input[value="' + method +'"]');
385
+ var deploy_button = $(stack_form).find('button:submit');
386
+ var current_status = $(deploy_button).find("span").text();
387
+ var deploy_text = "<span style='color: yellow'>" + who + " is deploying since "+
388
+ when + "</span>";
389
+
390
+ if (current_status != deploy_text) {
391
+ $(deploy_button).html(deploy_text);
392
+ }
393
+ }
394
+
395
+ // Puts the original text back in each button
396
+ function removeStackLockStatus() {
397
+ $(deploy_buttons).each(function(i,b) {
398
+ b.innerText = b.originalInnerText;
399
+ });
400
+ }
401
+
402
+ // Disable deploy buttons for that stack - store the active ones first!
403
+ function disableDeploys(){
404
+ if (deploy_enabled) {
405
+ deploy_enabled = false;
406
+ $(deploy_buttons).attr('disabled','disabled').addClass('button-disabled');
407
+ $("#unlockstack").show();
408
+ }
409
+ }
410
+
411
+ // Enable deploy buttons for that stack - store the active ones first!
412
+ function enableDeploys(){
413
+ if ((!deploy_enabled) && (!is_deploying)) {
414
+ $(deploy_buttons).removeAttr('disabled').removeClass('button-disabled');
415
+ deploy_enabled = true;
416
+ $("#unlockstack").hide();
417
+ }
418
+ }
419
+
420
+ // Poll our endpoint to see if this stack is locked. Update the UI accordingly
421
+ function updateDeployLocks(open_streamer) {
422
+ if ('{{stack}}' != '') {
423
+ if (!is_deploying) {
424
+ $.ajax({
425
+ url : '/{{stack}}/can-deploy',
426
+ dataType: 'json',
427
+ timeout: 1001,
428
+ success: function (data) {
429
+ if (data.can_deploy) {
430
+ enableDeploys();
431
+ removeStackLockStatus();
432
+ } else {
433
+ // This is only called once on page load to determine if log streamer
434
+ // should be opened
435
+ var form = $("form[data-method='" + data.method + "']");
436
+ if (open_streamer && data.who == '{{username}}' && form.length == 1) {
437
+ stream_log_websocket(form[0]);
438
+ } else {
439
+ disableDeploys();
440
+ addStackLockStatus(data.method, data.who, data.lock_time);
441
+ }
442
+ }
443
+ }
444
+ });
445
+ }
446
+ }
447
+ }
448
+
449
+ var getTime = function() {
450
+ var date = new Date();
451
+ return date.getFullYear() + "-"
452
+ + date.getMonth() + "-"
453
+ + date.getDay() + " "
454
+ + date.getHours() + ":"
455
+ + date.getMinutes() + ":"
456
+ + date.getSeconds();
457
+ }
458
+
459
+ var log = function(_elem, txt) {
460
+ var txtHolder = $('<pre></pre>');
461
+ var logLi = $("<li></li>");
462
+ var elem = $(_elem);
463
+ var platform = elem.attr('className');
464
+ var time = getTime();
465
+ var who = elem.find('input[name=who]').val()
466
+ var msg = elem.find('input[name=msg]').val()
467
+ logLi.text([platform, time, who, msg].join(' | '));
468
+ logLi.append(txtHolder.text(txt));
469
+ logLi.prependTo('#log ul');
470
+ }
471
+
472
+ // This block is for general stuff that needs to be set up on each page
473
+ // pollers are set in here with setInterval
474
+ $(function () {
475
+ $("#unlockstack").hide();
476
+
477
+ // Sets the app context
478
+ app_context = $('#app_context').data('context');
479
+
480
+ // Save the currently active deploy buttons with their original text
481
+ deploy_buttons = $('.stack-box form .stack-push button:submit:not(.button-disabled)');
482
+ $(deploy_buttons).each(function (i, b) {
483
+ b.originalInnerText = b.innerText;
484
+ });
485
+
486
+ updateDeployLocks(true);
487
+
488
+ setInterval(function() {
489
+ if (!autoScroll) { return; }
490
+ if ($("iframe").length === 0) {
491
+ return;
492
+ }
493
+ $("iframe").each(function() {
494
+ this.contentWindow.scrollTo(0, this.contentWindow.document.body.offsetHeight);
495
+ });
496
+ }, 200);
497
+
498
+ // Keep checking our endpoint to see if a deploy is in progress
499
+ setInterval(updateDeployLocks, 2000);
500
+
501
+ // don't do this if there isn't an error_report element on the page
502
+ if ($("#error_report").length > 0) {
503
+ // watch for stderror streaming by and copy them to an error_report div
504
+ // similarly info streams will be copied to an info report div
505
+ // this can be stopped with clearInterval(ec)
506
+ ec = setInterval(function() {
507
+ var errors = $("iframe").contents().find(".stderr");
508
+ if (errors.length && (seen_errors === 0)) {
509
+ // we only really need to do this one time
510
+ $("#error_report").append("<h1>There were some errors during this " +
511
+ current_method + " deploy</h1>");
512
+ $("#error_report").show();
513
+ }
514
+ var iw = 0; // i watcher, cause i scopes out
515
+ for (var i=1; i <= errors.length; i++) {
516
+ // this helps not show the same error more than once
517
+ // note we start with 1 not 0 and compensate -1 below
518
+ if (i > seen_errors) {
519
+ // we clone so that the error is still in the iframe too
520
+ var e = $(errors[i-1]).clone();
521
+ var t = e.text(e.text().replace(/STDERR:/,'ERROR: '));
522
+ $("#error_report").append(e);
523
+ }
524
+ iw = i;
525
+ }
526
+ seen_errors = iw;
527
+
528
+ }, 333); // 1/3 of a second should be faster than the human eye
529
+ }
530
+ });
531
+
532
+ $("iframe").click(function() {
533
+ autoScroll = !autoScroll;
534
+ });
535
+
536
+ $("#auto_scroll").click(function() {
537
+ autoScroll = this.checked;
538
+ });
539
+
540
+ function pad (n) {
541
+ return n<10 ? '0'+n : n;
542
+ }
543
+
544
+ /** Public: show a notification message
545
+ *
546
+ * Parameters:
547
+ * info_message - String to display
548
+ *
549
+ * Returns nothing
550
+ */
551
+ function ui_notify(info_message) {
552
+ var info_box = $("#info_msg_report");
553
+ if (info_box.is(':empty')) {
554
+ info_box.append("<h1>Deploy notifications</h1>");
555
+ }
556
+ info_box.append(info_message);
557
+ info_box.show();
558
+ }
559
+
560
+ /** Public: show a notification message and do some
561
+ * housekeeping for done deploys.
562
+ *
563
+ * Parameters:
564
+ * info_message - String to display at the end of a
565
+ * deploy
566
+ *
567
+ * Returns nothing
568
+ */
569
+ function deploy_done(info_message, method) {
570
+ var $panel = getDeployPanel(method);
571
+ if ($panel.length > 0 ) {
572
+ $panel.find('.progress-panel').hide();
573
+ $panel.find('.complete-panel').show();
574
+ } else {
575
+ ui_notify(info_message);
576
+ }
577
+
578
+ window.onbeforeunload = null;
579
+ is_deploying = false;
580
+ enableDeploys();
581
+ deploy_enabled = true;
582
+ current_method = "";
583
+ }
584
+
585
+ // clear the deploy lock
586
+ $('.line').on('click', '#unlockstack', function(evt) {
587
+ var msg = 'Are you sure you want to clear the deploy lock?';
588
+ if (is_deploying) {
589
+ msg = '**A DEPLOY IS IN PROGRESS**\n\n' + msg;
590
+ }
591
+
592
+ var confirmed = confirm(msg);
593
+ if (confirmed) {
594
+ clearing_deploy_lock = true;
595
+ window.location='/{{stack}}/remove-lock';
596
+ } else {
597
+ evt.preventDefault();
598
+ }
599
+ });
600
+ </script>
601
+ </div>
602
+ {{{ additional_bottom_body_html }}}
603
+ </body>
604
+ </html>