gush-control 0.1.1
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 +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
|