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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3171bdb3e43daa544ed8588ee72d2d37481ac667
4
+ data.tar.gz: ae5e2f8dc1416749174539fdaf5a6159168cf987
5
+ SHA512:
6
+ metadata.gz: 790d8184780fcf8f17ffde0a96375bdaaf948f8c6af70123af7bac17553120855391c1ef92b1cd5149b50457efbe9374809706409121ba8f8dd2960af39bf279
7
+ data.tar.gz: 1fb9815f197e66a45621d47630476382efc2557a4ba0b62f28e87687ac5d8ca8eec95439e668d1c18263e958c1af1e06d1f50c9ad4b5882a7e8c2d4e31c4ac2a
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ Gushfile.rb
24
+ dump.rdb
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gush-control.gemspec
4
+ gemspec
5
+
6
+ gem "gush", git: "git@github.com:lonelyplanet/gush.git"
7
+ gem "tilt", "~> 1.4.1"
8
+ gem "bbq-spawn", git: "git@github.com:drugpl/bbq-spawn.git"
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Chaps sp. z o.o.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Gush::Control
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'gush-control'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install gush-control
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/gush-control/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "gush/control"
4
+
5
+ map "/" do
6
+ run Gush::Control::App
7
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gush/control/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gush-control"
8
+ spec.version = Gush::Control::VERSION
9
+ spec.authors = ["Michal Krzyzanowski"]
10
+ spec.email = ["michal.krzyzanowski+github@gmail.com"]
11
+ spec.summary = "Web GUI for controlling Gush workflows"
12
+ spec.description = spec.summary
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "sinatra"
22
+ spec.add_runtime_dependency "sinatra-assetpack"
23
+ spec.add_runtime_dependency "thin"
24
+ spec.add_runtime_dependency "tilt"
25
+ spec.add_runtime_dependency "gush"
26
+ spec.add_runtime_dependency "slim"
27
+ spec.add_runtime_dependency "coffee-script"
28
+ spec.add_runtime_dependency "sinatra-websocket"
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.6"
31
+ spec.add_development_dependency "rake"
32
+ spec.add_development_dependency "rspec"
33
+ spec.add_development_dependency "capybara-webkit"
34
+ end
@@ -0,0 +1,20 @@
1
+ require "pathname"
2
+ require "thin"
3
+ require "sinatra"
4
+ require "coffee-script"
5
+ require "slim"
6
+ require "gush"
7
+ require "sinatra-websocket"
8
+ require "sinatra/assetpack"
9
+ require "gush/control/version"
10
+ require "gush/control/app"
11
+ require "gush/control/cli_extension"
12
+ require "gush/control/log_sender"
13
+
14
+ module Gush
15
+ module Control
16
+ def self.rackup_path
17
+ Pathname(__FILE__).dirname.parent.parent.join("config.ru")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,189 @@
1
+ require "sidekiq/api"
2
+
3
+ module Gush
4
+ module Control
5
+ class App < Sinatra::Base
6
+ enable :logging
7
+ set :server, :thin
8
+ set :client, proc { Gush::Client.new }
9
+
10
+ register Sinatra::AssetPack
11
+
12
+ assets {
13
+ serve '/js', from: 'assets/javascripts'
14
+ serve '/css', from: 'assets/stylesheets'
15
+ serve '/images', from: 'assets/images'
16
+ }
17
+
18
+ get "/" do
19
+ @workflows = settings.client.all_workflows
20
+ slim :index
21
+ end
22
+
23
+ get "/jobs/:workflow_id.:job" do |workflow_id, job|
24
+ @workflow = settings.client.find_workflow(workflow_id)
25
+ @job = @workflow.find_job(job)
26
+ slim :job
27
+ end
28
+
29
+ get '/logs/?:channel' do |channel|
30
+ request.websocket do |ws|
31
+ commands = Queue.new
32
+ tid = LogSender.new(ws,
33
+ redis,
34
+ commands,
35
+ channel).run
36
+
37
+ ws.onmessage do |msg|
38
+ commands.push(msg)
39
+ end
40
+
41
+ ws.onclose do
42
+ Thread.kill(tid)
43
+ end
44
+ end
45
+ end
46
+
47
+ get '/subscribe/?:channel' do |channel|
48
+ channel = channel.to_sym
49
+ request.websocket do |ws|
50
+ ws.onmessage do |msg|
51
+ EM.next_tick { ws.send(msg.to_json) }
52
+ end
53
+
54
+ tid = Thread.new do
55
+ redis.subscribe("gush.#{channel}") do |on|
56
+ on.message do |redis_channel, message|
57
+ EM.next_tick{ ws.send(message) }
58
+ end
59
+ end
60
+ end
61
+
62
+ ws.onclose do
63
+ Thread.kill(tid)
64
+ end
65
+ end
66
+ end
67
+
68
+ get "/workers" do
69
+ ps = Sidekiq::ProcessSet.new
70
+
71
+ request.websocket do |ws|
72
+ tid = Thread.new do
73
+ loop do
74
+ data = ps.map{|process| {host: process["hostname"], pid: process["pid"], jobs: process["busy"] } }.to_json
75
+ EM.next_tick{ ws.send(data) }
76
+ sleep 5
77
+ end
78
+ end
79
+
80
+ ws.onclose do
81
+ Thread.kill(tid)
82
+ end
83
+ end
84
+ end
85
+
86
+ get "/show/:workflow.?:format?" do |id, format|
87
+ @workflow = settings.client.find_workflow(id)
88
+
89
+
90
+ @links = []
91
+ @jobs = []
92
+ @jobs << {name: "Start"}
93
+ @jobs << {name: "End"}
94
+
95
+
96
+ @workflow.jobs.each do |job|
97
+ name = job.class.to_s
98
+ @jobs << {
99
+ name: name,
100
+ finished: job.finished?,
101
+ running: job.running?,
102
+ enqueued: job.enqueued?,
103
+ failed: job.failed?
104
+ }
105
+
106
+ if job.incoming.empty?
107
+ @links << {source: "Start", target: name, type: "flow"}
108
+ end
109
+
110
+ job.outgoing.each do |out|
111
+ @links << {source: name, target: out, type: "flow"}
112
+ end
113
+
114
+ if job.outgoing.empty?
115
+ @links << {source: name, target: "End", type: "flow"}
116
+ end
117
+ end
118
+
119
+ if format == "json"
120
+ content_type :json
121
+ return { jobs: @jobs, links: @links }.to_json
122
+ end
123
+
124
+ slim :show
125
+ end
126
+
127
+ post "/start/:workflow/?:job?" do |id, job|
128
+ workflow = settings.client.find_workflow(id)
129
+ settings.client.start_workflow(workflow, Array(job))
130
+ content_type :json
131
+ workflow.to_json
132
+ end
133
+
134
+ post "/stop/:workflow" do |workflow|
135
+ settings.client.stop_workflow(workflow)
136
+ content_type :json
137
+ workflow.to_json
138
+ end
139
+
140
+ post "/create/:workflow" do |name|
141
+ workflow = settings.client.create_workflow(name)
142
+ content_type :json
143
+ workflow.to_json
144
+ end
145
+
146
+ post "/destroy/:workflow" do |id|
147
+ workflow = settings.client.find_workflow(id)
148
+ remove_workflow_and_logs(workflow)
149
+ {status: "ok"}.to_json
150
+ end
151
+
152
+ post "/purge_logs/:channel" do |channel|
153
+ remove_logs_in_channel(channel)
154
+
155
+ {status: "ok"}.to_json
156
+ end
157
+
158
+ post "/purge" do
159
+ completed = settings.client.all_workflows.select(&:finished?)
160
+ completed.each {|workflow| remove_workflow_and_logs(workflow) }
161
+
162
+ {status: "ok"}.to_json
163
+ end
164
+
165
+ private
166
+
167
+ def redis
168
+ Thread.current[:redis] ||= Redis.new(url: settings.client.configuration.redis_url)
169
+ end
170
+
171
+ def remove_workflow_and_logs(workflow)
172
+ remove_workflow(workflow)
173
+ remove_logs(workflow)
174
+ end
175
+
176
+ def remove_workflow(workflow)
177
+ settings.client.destroy_workflow(workflow)
178
+ end
179
+
180
+ def remove_logs(workflow)
181
+ redis.keys("gush.logs.#{workflow.id}.*").each {|key| redis.del(key) }
182
+ end
183
+
184
+ def remove_logs_in_channel(channel)
185
+ redis.del("gush.logs.#{channel}")
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,59 @@
1
+ window.Gush = new Gush
2
+ $(document).ready ->
3
+ window.Gush.initialize()
4
+ Foundation.global.namespace = ''
5
+ $(document).foundation()
6
+
7
+ $(this).on "click", ".button.start-workflow", (event) ->
8
+ event.preventDefault()
9
+ if !$(event.target).is(".button")
10
+ return
11
+ if($(this).data("action") == "start")
12
+ Gush.startWorkflow($(this).data("workflow-id"), $(this))
13
+ else
14
+ Gush.stopWorkflow($(this).data("workflow-id"), $(this))
15
+
16
+ $(this).on "click", ".start-job", (event) ->
17
+ event.preventDefault()
18
+ Gush.startJob($(this).data("workflow-id"), $(this).data("job-name"), $(this))
19
+
20
+ $(this).on "click", ".create-workflow", (event) ->
21
+ event.preventDefault()
22
+ Gush.createWorkflow($(this).data("workflow-class"))
23
+
24
+ $(this).on "click", ".destroy-workflow", (event) ->
25
+ event.preventDefault()
26
+ Gush.destroyWorkflow($(this).data("workflow-id"), $(this))
27
+
28
+ $(this).on "click", ".retry-workflow", (event) ->
29
+ event.preventDefault()
30
+ Gush.retryWorkflow($(this).data("workflow-id"), $(this))
31
+
32
+ $(this).on "dblclick", "svg .node", (event) ->
33
+ event.preventDefault()
34
+ workflow_id = $(this).closest('svg').data('workflow-id')
35
+ name = $(this).data('job-name')
36
+ if name isnt "Start" and name isnt "End"
37
+ window.location.href = "/jobs/#{workflow_id}.#{name}"
38
+
39
+ $(this).on "click", ".jobs-filter dd a", (event) ->
40
+ event.preventDefault()
41
+ filter = $(this).html().toLowerCase()
42
+ table = $("table.nodes tbody")
43
+
44
+ $(this).closest('dl').find('dd').removeClass('active')
45
+ $(this).parent().addClass('active')
46
+
47
+ table.find("tr").hide()
48
+ if filter == "all"
49
+ table.find("tr").show()
50
+ else
51
+ table.find("tr.#{filter}").show()
52
+
53
+ $(this).on "click", "a.remove-completed", (event) ->
54
+ event.preventDefault()
55
+ Gush.removeCompleted()
56
+
57
+ $(this).on "click", "a.remove-logs", (event) ->
58
+ event.preventDefault()
59
+ Gush.removeLogs($(this).data('workflow-id'), $(this).data('job-name'))
@@ -0,0 +1,58 @@
1
+ class @Graph
2
+ constructor: (canvas_id) ->
3
+ @canvas = "##{canvas_id}"
4
+ @digraph = new dagreD3.Digraph
5
+
6
+ populate: (nodes, links) ->
7
+ nodes.forEach (node) =>
8
+ @digraph.addNode node.name,
9
+ finished: node.finished,
10
+ failed: node.failed,
11
+ running: node.running,
12
+ enqueued: node.enqueued
13
+ label: node.name
14
+
15
+ links.forEach (edge) =>
16
+ @digraph.addEdge(null, edge.source, edge.target)
17
+
18
+ render: ->
19
+ renderer = new dagreD3.Renderer
20
+ layout = dagreD3.layout().nodeSep(50).rankDir("LR");
21
+ oldDrawNodes = renderer.drawNodes()
22
+
23
+ renderer.drawNodes (graph, root) =>
24
+ svgNodes = oldDrawNodes(graph, root);
25
+ svgNodes.attr "data-job-name", (name) =>
26
+ name;
27
+
28
+ svgNodes.attr "class", (name) =>
29
+ node = @digraph.node(name)
30
+ classes = "node " + name.replace(/::/g, '_').toLowerCase()
31
+ if node.failed
32
+ classes += " status-failed";
33
+ else if node.finished
34
+ classes += " status-finished";
35
+ else if node.running
36
+ classes += " status-running";
37
+ else if node.enqueued
38
+ classes += " status-enqueued"
39
+ classes;
40
+
41
+ svgNodes;
42
+ .layout(layout)
43
+ .run(@digraph, d3.select("#{@canvas} g"));
44
+ @panZoom()
45
+
46
+ panZoom: ->
47
+ svgPanZoom @canvas,
48
+ panEnabled: true,
49
+ minZoom: 0.8,
50
+ maxZoom: 10,
51
+ zoomEnabled: true,
52
+ center: false,
53
+ fit: true
54
+
55
+ markNode: (name, class_names) ->
56
+ name = name.replace(/::/g, '_').toLowerCase()
57
+ $("svg#{@canvas} .node.#{name}")
58
+ .attr('class', "node #{name} #{class_names}")