gush-control 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/config.ru +7 -0
- data/gush-control.gemspec +34 -0
- data/lib/gush/control.rb +20 -0
- data/lib/gush/control/app.rb +189 -0
- data/lib/gush/control/assets/images/.gitkeep +0 -0
- data/lib/gush/control/assets/javascripts/.gitkeep +0 -0
- data/lib/gush/control/assets/javascripts/application.coffee +59 -0
- data/lib/gush/control/assets/javascripts/graph.coffee +58 -0
- data/lib/gush/control/assets/javascripts/gush.coffee +259 -0
- data/lib/gush/control/assets/javascripts/job.coffee +36 -0
- data/lib/gush/control/assets/javascripts/machine.coffee +37 -0
- data/lib/gush/control/assets/javascripts/templates.coffee +26 -0
- data/lib/gush/control/assets/javascripts/vendor/d3.min.js +5 -0
- data/lib/gush/control/assets/javascripts/vendor/dagre-d3.min.js +2 -0
- data/lib/gush/control/assets/javascripts/vendor/foundation.min.js +10 -0
- data/lib/gush/control/assets/javascripts/vendor/jquery.js +6 -0
- data/lib/gush/control/assets/javascripts/vendor/modernizr.js +8 -0
- data/lib/gush/control/assets/javascripts/vendor/moment.js +6 -0
- data/lib/gush/control/assets/javascripts/vendor/mustache.min.js +1 -0
- data/lib/gush/control/assets/javascripts/vendor/sugar.min.js +132 -0
- data/lib/gush/control/assets/javascripts/vendor/svg-pan-zoom.min.js +1 -0
- data/lib/gush/control/assets/javascripts/view.coffee +32 -0
- data/lib/gush/control/assets/javascripts/workflow.coffee +49 -0
- data/lib/gush/control/assets/stylesheets/.gitkeep +0 -0
- data/lib/gush/control/assets/stylesheets/app.css +79 -0
- data/lib/gush/control/assets/stylesheets/foundation.min.css +1 -0
- data/lib/gush/control/assets/stylesheets/normalize.css +425 -0
- data/lib/gush/control/cli_extension.rb +23 -0
- data/lib/gush/control/log_sender.rb +67 -0
- data/lib/gush/control/logger.rb +84 -0
- data/lib/gush/control/logger_extension.rb +19 -0
- data/lib/gush/control/version.rb +5 -0
- data/lib/gush/control/views/index.slim +29 -0
- data/lib/gush/control/views/job.slim +32 -0
- data/lib/gush/control/views/layout.slim +96 -0
- data/lib/gush/control/views/show.slim +62 -0
- data/spec/acceptance_spec.rb +68 -0
- data/spec/redis.conf +2 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/test_user.rb +59 -0
- metadata +262 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gush
|
2
|
+
class CLI < Thor
|
3
|
+
desc "server [options]", "Start server"
|
4
|
+
option :port, aliases: "-p", default: "3000"
|
5
|
+
option :address, aliases: "-a", default: "0.0.0.0"
|
6
|
+
option :daemonize, aliases: "-d"
|
7
|
+
|
8
|
+
def server
|
9
|
+
Thin::Runner.new(params_to_args(thin_params(options))).run!
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def thin_params(options)
|
14
|
+
{ rackup: Gush::Control.rackup_path.to_s, port: options[:port], address: options[:address] }.tap do |params|
|
15
|
+
params.merge!(daemonize: true) if options[:daemonize]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def params_to_args(params)
|
20
|
+
params.flat_map{|k, v| ["--#{k}", v] }.unshift("start")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class LogSender
|
2
|
+
ROWS = 25
|
3
|
+
|
4
|
+
def initialize(socket, redis, commands, channel)
|
5
|
+
@redis = redis
|
6
|
+
@commands = commands
|
7
|
+
@channel = channel
|
8
|
+
@socket = socket
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
Thread.new do
|
13
|
+
tail = [0, message_count - ROWS].max
|
14
|
+
head = (tail - 1).downto(0).each_slice(ROWS)
|
15
|
+
|
16
|
+
loop do
|
17
|
+
if commands.empty?
|
18
|
+
method = :append
|
19
|
+
logs = fetch_logs_after(tail)
|
20
|
+
tail += logs.size
|
21
|
+
else
|
22
|
+
case commands.pop
|
23
|
+
when 'prepend'
|
24
|
+
method = :prepend
|
25
|
+
begin
|
26
|
+
logs = fetch_range(head.next)
|
27
|
+
rescue StopIteration
|
28
|
+
logs = []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
send_lines(sanitize_logs(logs), method)
|
34
|
+
sleep 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def send_lines(logs, method)
|
42
|
+
data = {lines: logs, method: method}.to_json
|
43
|
+
EM.next_tick{ socket.send(data) } if logs.any?
|
44
|
+
end
|
45
|
+
|
46
|
+
def message_count
|
47
|
+
redis.llen(redis_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_logs_after(idx)
|
51
|
+
redis.lrange(redis_key, idx, idx + ROWS)
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_range(r)
|
55
|
+
redis.lrange(redis_key, r.last, r.first).reverse
|
56
|
+
end
|
57
|
+
|
58
|
+
def redis_key
|
59
|
+
"gush.logs.#{channel}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def sanitize_logs(lines)
|
63
|
+
lines.map {|l| Rack::Utils.escape_html(l) }
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :channel, :redis, :commands, :socket
|
67
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Gush
|
5
|
+
module Control
|
6
|
+
class Logger
|
7
|
+
include ::Logger::Severity
|
8
|
+
|
9
|
+
attr_accessor :level, :progname
|
10
|
+
|
11
|
+
def initialize(redis, channel, level = DEBUG)
|
12
|
+
@progname = nil
|
13
|
+
@redis = redis
|
14
|
+
@level = level
|
15
|
+
@channel = channel
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(severity = UNKNOWN, message = nil, prog = nil, &block)
|
19
|
+
return true if severity < level
|
20
|
+
|
21
|
+
if message.nil?
|
22
|
+
if block_given?
|
23
|
+
message = yield
|
24
|
+
else
|
25
|
+
message = prog
|
26
|
+
prog = progname
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
write(format_message(severity, prog || progname, message))
|
31
|
+
true
|
32
|
+
end
|
33
|
+
alias log add
|
34
|
+
|
35
|
+
def <<(message)
|
36
|
+
write(message)
|
37
|
+
end
|
38
|
+
|
39
|
+
def debug(message = nil, &block)
|
40
|
+
add(DEBUG, message, nil, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def info(progname = nil, &block)
|
44
|
+
add(INFO, nil, progname, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def warn(progname = nil, &block)
|
48
|
+
add(WARN, nil, progname, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def error(progname = nil, &block)
|
52
|
+
add(ERROR, nil, progname, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def fatal(progname = nil, &block)
|
56
|
+
add(FATAL, nil, progname, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def unknown(progname = nil, &block)
|
60
|
+
add(UNKNOWN, nil, progname, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def close
|
64
|
+
# noop
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
LABELS = %w(DEBUG INFO WARN ERROR FATAL ANY)
|
70
|
+
|
71
|
+
attr_reader :redis, :channel
|
72
|
+
|
73
|
+
def write(message)
|
74
|
+
redis.rpush(channel, message)
|
75
|
+
end
|
76
|
+
|
77
|
+
def format_message(severity, prog, message)
|
78
|
+
current_time = Time.now.utc
|
79
|
+
severity = LABELS[severity]
|
80
|
+
"%s, [%s.%s #%s] %5s -- %s: %s\n" % [severity[0], current_time.iso8601, current_time.usec, $$, severity, prog, message]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'gush/control/logger'
|
2
|
+
|
3
|
+
require 'gush/control'
|
4
|
+
|
5
|
+
module Gush
|
6
|
+
module Control
|
7
|
+
class LoggerBuilder < Gush::LoggerBuilder
|
8
|
+
def build
|
9
|
+
Logger.new(Redis.new(url: Gush.configuration.redis_url), "gush.logs.#{workflow.id}.#{job.name}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Gush::Workflow
|
16
|
+
def default_logger_builder
|
17
|
+
Gush::Control::LoggerBuilder
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
.row
|
2
|
+
.large-12.columns
|
3
|
+
h1
|
4
|
+
'Registered workflows
|
5
|
+
a.remove-completed.radius.button.alert.right href="#" Purge completed
|
6
|
+
|
7
|
+
table.workflows data-workflows=@workflows.to_json
|
8
|
+
thead
|
9
|
+
th ID
|
10
|
+
th Name
|
11
|
+
th Progress
|
12
|
+
th Status
|
13
|
+
th Started at
|
14
|
+
th Finished at
|
15
|
+
th Action
|
16
|
+
|
17
|
+
tbody
|
18
|
+
tr
|
19
|
+
td.text-center colspan=4 Loading…
|
20
|
+
h1 Registered machines
|
21
|
+
|
22
|
+
table.machines
|
23
|
+
thead
|
24
|
+
th PID
|
25
|
+
th Host
|
26
|
+
th Enqueued jobs
|
27
|
+
th Status
|
28
|
+
tbody
|
29
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
.row
|
2
|
+
.large-8.columns
|
3
|
+
h1
|
4
|
+
- case
|
5
|
+
- when @job.failed?
|
6
|
+
span.label.alert Failed
|
7
|
+
- when @job.enqueued?
|
8
|
+
span.label.secondary Enqueued
|
9
|
+
- when @job.running?
|
10
|
+
span.label Running
|
11
|
+
- when @job.finished?
|
12
|
+
span.label.success Finished
|
13
|
+
- else
|
14
|
+
span.label.secondary Pending
|
15
|
+
= @job.name
|
16
|
+
'
|
17
|
+
small = @workflow.id
|
18
|
+
'
|
19
|
+
small
|
20
|
+
a href="/show/#{@workflow.id}" Back to workflow
|
21
|
+
.large-4.columns
|
22
|
+
br
|
23
|
+
a.button.alert.small.radius.remove-logs data-workflow-id=@workflow.id data-job-name=@job.name
|
24
|
+
| Remove logs
|
25
|
+
- if @job.finished? && @job.failed?
|
26
|
+
a.button.alert.small.radius.start-job data-workflow-id=@workflow.id data-job-name=@job.name
|
27
|
+
| Restart job
|
28
|
+
h2 Logs
|
29
|
+
ul.logs
|
30
|
+
|
31
|
+
javascript:
|
32
|
+
Gush.registerLogsSocket("#{@workflow.id}", "#{@job.class.name}")
|
@@ -0,0 +1,96 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title Gush control
|
5
|
+
script type="text/javascript" src="/js/vendor/modernizr.js"
|
6
|
+
script type="text/javascript" src="/js/vendor/mustache.min.js"
|
7
|
+
script type="text/javascript" src="/js/vendor/moment.js"
|
8
|
+
script type="text/javascript" src="/js/vendor/jquery.js"
|
9
|
+
script type="text/javascript" src="/js/vendor/sugar.min.js"
|
10
|
+
script type="text/javascript" src="/js/vendor/foundation.min.js"
|
11
|
+
script type="text/javascript" src="/js/vendor/d3.min.js"
|
12
|
+
script type="text/javascript" src="/js/vendor/svg-pan-zoom.min.js"
|
13
|
+
script type="text/javascript" src="/js/vendor/dagre-d3.min.js"
|
14
|
+
script type="text/javascript" src="/js/gush.js"
|
15
|
+
script type="text/javascript" src="/js/job.js"
|
16
|
+
script type="text/javascript" src="/js/machine.js"
|
17
|
+
script type="text/javascript" src="/js/workflow.js"
|
18
|
+
script type="text/javascript" src="/js/graph.js"
|
19
|
+
script type="text/javascript" src="/js/templates.js"
|
20
|
+
script type="text/javascript" src="/js/view.js"
|
21
|
+
script type="text/javascript" src="/js/application.js"
|
22
|
+
link type="text/css" rel="stylesheet" href="/css/normalize.css"
|
23
|
+
link type="text/css" rel="stylesheet" href="/css/foundation.min.css"
|
24
|
+
link type="text/css" rel="stylesheet" href="/css/app.css"
|
25
|
+
body
|
26
|
+
nav class="top-bar fixed" data-topbar=true
|
27
|
+
ul class="title-area"
|
28
|
+
li class="name"
|
29
|
+
h1
|
30
|
+
a href="/" Gush
|
31
|
+
li class="toggle-topbar menu-icon"
|
32
|
+
a href="#"
|
33
|
+
span Menu
|
34
|
+
|
35
|
+
section class="top-bar-section"
|
36
|
+
ul class="left"
|
37
|
+
li class="has-dropdown"
|
38
|
+
a href="#" Create workflow
|
39
|
+
ul class="dropdown"
|
40
|
+
- Gush::Workflow.descendants.each do |workflow|
|
41
|
+
li
|
42
|
+
a.create-workflow data-workflow-class=workflow href="#" = workflow
|
43
|
+
== yield
|
44
|
+
#modalBox.reveal-modal.text-center data-reveal="data-reval" data-reveal-id="modalBox"
|
45
|
+
.data
|
46
|
+
a.close-reveal-modal ×
|
47
|
+
|
48
|
+
script id="progress-template" type="x-tmpl-mustache"
|
49
|
+
.nice.progress.round
|
50
|
+
span.meter.label style="width: {{progress}}%"
|
51
|
+
script id="status-template" type="x-tmpl-mustache"
|
52
|
+
span class="round label {{class}}"
|
53
|
+
| {{status}}
|
54
|
+
script id="workflow-template" type="x-tmpl-mustache"
|
55
|
+
tr id="{{id}}" data-name="{{name}}" data-finished="{{finished}}" data-total="{{total}}"
|
56
|
+
td
|
57
|
+
a href="/show/{{id}}"
|
58
|
+
| {{id}}
|
59
|
+
td
|
60
|
+
| {{name}}
|
61
|
+
td.progress-container
|
62
|
+
| {{> progress}}
|
63
|
+
td.status
|
64
|
+
| {{> status}}
|
65
|
+
td.started_at
|
66
|
+
| {{started_at}}
|
67
|
+
td.finished_at
|
68
|
+
| {{finished_at}}
|
69
|
+
td
|
70
|
+
| {{> action}}
|
71
|
+
|
72
|
+
|
73
|
+
script id="workflow-action-template" type="x-tmpl-mustache"
|
74
|
+
a class="button tiny radius start-workflow {{classes}}" data-action="{{action}}" data-workflow-id="{{workflow_id}}"
|
75
|
+
| {{description}}
|
76
|
+
|
77
|
+
script id="node-template" type="x-tmpl-mustache"
|
78
|
+
tr class="{{class}}"
|
79
|
+
td
|
80
|
+
| {{name}}
|
81
|
+
td
|
82
|
+
| {{status}}
|
83
|
+
td
|
84
|
+
| {{started_at}}
|
85
|
+
td
|
86
|
+
| {{finished_at}}
|
87
|
+
script id="machine-template" type="x-tmpl-mustache"
|
88
|
+
tr data-pid="{{pid}}"
|
89
|
+
td
|
90
|
+
| {{pid}}
|
91
|
+
td
|
92
|
+
| {{host}}
|
93
|
+
td
|
94
|
+
| {{jobs}}
|
95
|
+
td
|
96
|
+
| {{status}}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
.row
|
2
|
+
.large-8.columns
|
3
|
+
h1.workflow-title
|
4
|
+
= @workflow.class
|
5
|
+
'
|
6
|
+
small = @workflow.id
|
7
|
+
'
|
8
|
+
small
|
9
|
+
a href="/" Back to list
|
10
|
+
.large-4.columns
|
11
|
+
br
|
12
|
+
- if !@workflow.started?
|
13
|
+
a.button.split.success.small.radius.start-workflow data-action="start" data-workflow-id=@workflow.id
|
14
|
+
| Start workflow
|
15
|
+
span data-dropdown="drop"
|
16
|
+
- else
|
17
|
+
a.button.split.alert.small.radius.start-workflow data-action="stop" data-workflow-id=@workflow.id
|
18
|
+
| Stop workflow
|
19
|
+
span data-dropdown="drop"
|
20
|
+
ul#drop.f-dropdown data-dropdown-content=true
|
21
|
+
li
|
22
|
+
a.retry-workflow data-workflow-id=@workflow.id href="#" Restart failed jobs
|
23
|
+
li
|
24
|
+
a.destroy-workflow data-workflow-id=@workflow.id href="#" Delete workflow
|
25
|
+
.row
|
26
|
+
.large-12.columns
|
27
|
+
svg data-workflow-id=@workflow.id width="100%" height="400" id="canvas-#{@workflow.id}"
|
28
|
+
g class="viewport"
|
29
|
+
.row
|
30
|
+
.large-12.columns
|
31
|
+
h2 Jobs
|
32
|
+
dl.sub-nav.jobs-filter
|
33
|
+
dt Filter:
|
34
|
+
dd.active
|
35
|
+
a All
|
36
|
+
dd
|
37
|
+
a Waiting
|
38
|
+
dd
|
39
|
+
a Enqueued
|
40
|
+
dd
|
41
|
+
a Running
|
42
|
+
dd
|
43
|
+
a Failed
|
44
|
+
dd
|
45
|
+
a Finished
|
46
|
+
table.jobs
|
47
|
+
thead
|
48
|
+
th Name
|
49
|
+
th Status
|
50
|
+
th Started at
|
51
|
+
th Finished at
|
52
|
+
tbody
|
53
|
+
|
54
|
+
script
|
55
|
+
== "var jobs = #{JSON.dump(@jobs)};"
|
56
|
+
== "var links = #{JSON.dump(@links)};"
|
57
|
+
|
58
|
+
javascript:
|
59
|
+
|
60
|
+
graph = new Graph("canvas-#{@workflow.id}")
|
61
|
+
graph.populate(jobs, links)
|
62
|
+
graph.render()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature "creating workflows" do
|
4
|
+
let(:user) { TestUser.new }
|
5
|
+
scenario "user can create new workflow from list" do
|
6
|
+
user.create_workflow("TestWorkflow")
|
7
|
+
|
8
|
+
header = page.find('h1.workflow-title')
|
9
|
+
expect(header).to have_content 'TestWorkflow'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
feature "deleting workflows" do
|
14
|
+
before :each do
|
15
|
+
user.create_workflow("TestWorkflow")
|
16
|
+
end
|
17
|
+
|
18
|
+
scenario "user can delete workflow from workflow view" do
|
19
|
+
user.delete_workflow("TestWorkflow")
|
20
|
+
|
21
|
+
table = page.find("table.workflows")
|
22
|
+
expect(table).to_not have_content("TestWorkflow")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
feature "starting workflows" do
|
27
|
+
before :each do
|
28
|
+
user.create_workflow("TestWorkflow")
|
29
|
+
end
|
30
|
+
|
31
|
+
scenario "user can start workflow from homepage" do
|
32
|
+
user.start_workflow_from_homepage("TestWorkflow")
|
33
|
+
|
34
|
+
row = user.find_workflow_row("TestWorkflow")
|
35
|
+
expect(row).to have_content('Stop workflow')
|
36
|
+
end
|
37
|
+
|
38
|
+
scenario "user can start workflow from workflow view" do
|
39
|
+
user.go_to_workflow("TestWorkflow")
|
40
|
+
|
41
|
+
user.click_link_matching "a.start-workflow.success"
|
42
|
+
|
43
|
+
button = page.find('a.start-workflow.alert')
|
44
|
+
expect(button).to have_content("Stop workflow")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
feature "stopping workflows" do
|
49
|
+
before :each do
|
50
|
+
user.create_workflow("TestWorkflow")
|
51
|
+
user.start_workflow_from_homepage("TestWorkflow")
|
52
|
+
end
|
53
|
+
|
54
|
+
scenario "user stops workflow from homepage" do
|
55
|
+
user.stop_workflow_from_homepage("TestWorkflow")
|
56
|
+
|
57
|
+
row = user.find_workflow_row("TestWorkflow")
|
58
|
+
expect(row).to have_content('Start workflow')
|
59
|
+
end
|
60
|
+
|
61
|
+
scenario "user stops workflow from workflow view" do
|
62
|
+
user.go_to_workflow("TestWorkflow")
|
63
|
+
user.click_link_matching "a.start-workflow.alert"
|
64
|
+
|
65
|
+
button = page.find('a.start-workflow.success')
|
66
|
+
expect(button).to have_content("Start workflow")
|
67
|
+
end
|
68
|
+
end
|