gush-control 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/Gemfile +8 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +2 -0
  7. data/config.ru +7 -0
  8. data/gush-control.gemspec +34 -0
  9. data/lib/gush/control.rb +20 -0
  10. data/lib/gush/control/app.rb +189 -0
  11. data/lib/gush/control/assets/images/.gitkeep +0 -0
  12. data/lib/gush/control/assets/javascripts/.gitkeep +0 -0
  13. data/lib/gush/control/assets/javascripts/application.coffee +59 -0
  14. data/lib/gush/control/assets/javascripts/graph.coffee +58 -0
  15. data/lib/gush/control/assets/javascripts/gush.coffee +259 -0
  16. data/lib/gush/control/assets/javascripts/job.coffee +36 -0
  17. data/lib/gush/control/assets/javascripts/machine.coffee +37 -0
  18. data/lib/gush/control/assets/javascripts/templates.coffee +26 -0
  19. data/lib/gush/control/assets/javascripts/vendor/d3.min.js +5 -0
  20. data/lib/gush/control/assets/javascripts/vendor/dagre-d3.min.js +2 -0
  21. data/lib/gush/control/assets/javascripts/vendor/foundation.min.js +10 -0
  22. data/lib/gush/control/assets/javascripts/vendor/jquery.js +6 -0
  23. data/lib/gush/control/assets/javascripts/vendor/modernizr.js +8 -0
  24. data/lib/gush/control/assets/javascripts/vendor/moment.js +6 -0
  25. data/lib/gush/control/assets/javascripts/vendor/mustache.min.js +1 -0
  26. data/lib/gush/control/assets/javascripts/vendor/sugar.min.js +132 -0
  27. data/lib/gush/control/assets/javascripts/vendor/svg-pan-zoom.min.js +1 -0
  28. data/lib/gush/control/assets/javascripts/view.coffee +32 -0
  29. data/lib/gush/control/assets/javascripts/workflow.coffee +49 -0
  30. data/lib/gush/control/assets/stylesheets/.gitkeep +0 -0
  31. data/lib/gush/control/assets/stylesheets/app.css +79 -0
  32. data/lib/gush/control/assets/stylesheets/foundation.min.css +1 -0
  33. data/lib/gush/control/assets/stylesheets/normalize.css +425 -0
  34. data/lib/gush/control/cli_extension.rb +23 -0
  35. data/lib/gush/control/log_sender.rb +67 -0
  36. data/lib/gush/control/logger.rb +84 -0
  37. data/lib/gush/control/logger_extension.rb +19 -0
  38. data/lib/gush/control/version.rb +5 -0
  39. data/lib/gush/control/views/index.slim +29 -0
  40. data/lib/gush/control/views/job.slim +32 -0
  41. data/lib/gush/control/views/layout.slim +96 -0
  42. data/lib/gush/control/views/show.slim +62 -0
  43. data/spec/acceptance_spec.rb +68 -0
  44. data/spec/redis.conf +2 -0
  45. data/spec/spec_helper.rb +45 -0
  46. data/spec/test_user.rb +59 -0
  47. 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,5 @@
1
+ module Gush
2
+ module Control
3
+ VERSION = "0.1.1"
4
+ end
5
+ 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 &#215;
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