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.
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